diff --git a/core/core.services.yml b/core/core.services.yml index 183f7e2..9222667 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -502,7 +502,7 @@ services: arguments: ['@config.factory'] path.validator: class: Drupal\Core\Path\PathValidator - arguments: ['@router', '@router.route_provider', '@request_stack'] + arguments: ['@router', '@router.no_access_checks', '@current_user', '@path_processor_manager'] # The argument to the hashing service defined in services.yml, to the # constructor of PhpassHashedPassword is the log2 number of iterations for diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index fcc6476..4e4ba8c 100644 --- a/core/lib/Drupal.php +++ b/core/lib/Drupal.php @@ -666,4 +666,13 @@ public static function menuTree() { return static::$container->get('menu.link_tree'); } + /** + * Returns the path validator. + * + * @return \Drupal\Core\Path\PathValidatorInterface + */ + public static function pathValidator() { + return static::$container->get('path.validator'); + } + } diff --git a/core/lib/Drupal/Component/Utility/UrlHelper.php b/core/lib/Drupal/Component/Utility/UrlHelper.php index 4ff656a..0c0e07d 100644 --- a/core/lib/Drupal/Component/Utility/UrlHelper.php +++ b/core/lib/Drupal/Component/Utility/UrlHelper.php @@ -151,7 +151,12 @@ public static function parse($url) { if (strpos($url, '://') !== FALSE) { // Split off everything before the query string into 'path'. $parts = explode('?', $url); - $options['path'] = $parts[0]; + + // Don't support URLs without a path, like 'http://'. + list(, $path) = explode('://', $parts[0], 2); + if ($path != '') { + $options['path'] = $parts[0]; + } // If there is a query string, transform it into keyed query parameters. if (isset($parts[1])) { $query_parts = explode('#', $parts[1]); diff --git a/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php index ae5b285..ff1792b 100644 --- a/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php +++ b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Menu; use Drupal\Core\Access\AccessManagerInterface; +use Drupal\Core\Path\PathValidator; use Drupal\Core\Session\AccountInterface; /** @@ -93,6 +94,9 @@ public function checkAccess(array $tree) { * TRUE if the current user can access the link, FALSE otherwise. */ protected function menuLinkCheckAccess(MenuLinkInterface $instance) { + if ($this->account->hasPermission('link to any page')) { + return TRUE; + } // Use the definition here since that's a lot faster than creating a Url // object that we don't need. $definition = $instance->getPluginDefinition(); diff --git a/core/lib/Drupal/Core/Path/PathValidator.php b/core/lib/Drupal/Core/Path/PathValidator.php index d363417..b6c3a5a 100644 --- a/core/lib/Drupal/Core/Path/PathValidator.php +++ b/core/lib/Drupal/Core/Path/PathValidator.php @@ -2,18 +2,22 @@ /** * @file - * Contains Drupal\Core\Path\PathValidator + * Contains \Drupal\Core\Path\PathValidator */ namespace Drupal\Core\Path; use Drupal\Component\Utility\UrlHelper; use Drupal\Core\ParamConverter\ParamNotConvertedException; -use Drupal\Core\Routing\RequestHelper; -use Drupal\Core\Routing\RouteProviderInterface; -use Symfony\Component\HttpFoundation\RequestStack; +use Drupal\Core\PathProcessor\InboundPathProcessorInterface; +use Drupal\Core\Routing\AccessAwareRouterInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Url; +use Symfony\Cmf\Component\Routing\RouteObjectInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; -use Symfony\Component\Routing\Matcher\RequestMatcherInterface; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; +use Symfony\Component\Routing\Matcher\UrlMatcherInterface; /** * Provides a default path validator and access checker. @@ -21,66 +25,122 @@ class PathValidator implements PathValidatorInterface { /** - * The request matcher. + * The access aware router. * - * @var \Symfony\Component\Routing\Matcher\RequestMatcherInterface + * @var \Drupal\Core\Routing\AccessAwareRouterInterface */ - protected $requestMatcher; + protected $accessAwareRouter; /** - * The route provider. + * A router implementation which does not check access. * - * @var \Drupal\Core\Routing\RouteProviderInterface + * @var \Symfony\Component\Routing\Matcher\UrlMatcherInterface */ - protected $routeProvider; + protected $accessUnawareRouter; /** - * The request stack. + * The current user. * - * @var \Symfony\Component\HttpFoundation\RequestStack + * @var \Drupal\Core\Session\AccountInterface */ - protected $requestStack; + protected $account; + + /** + * The path processor. + * + * @var \Drupal\Core\PathProcessor\InboundPathProcessorInterface + */ + protected $pathProcessor; /** * Creates a new PathValidator. * - * @param \Symfony\Component\Routing\Matcher\RequestMatcherInterface $request_matcher - * The request matcher. - * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider - * The route provider. - * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack - * The request stack. + * @param \Drupal\Core\Routing\AccessAwareRouterInterface $access_aware_router + * The access aware router. + * @param \Symfony\Component\Routing\Matcher\UrlMatcherInterface $access_unaware_router + * A router implementation which does not check access. + * @param \Drupal\Core\Session\AccountInterface $account + * The current user. + * @param \Drupal\Core\PathProcessor\InboundPathProcessorInterface $path_processor + * The path processor; */ - public function __construct(RequestMatcherInterface $request_matcher, RouteProviderInterface $route_provider, RequestStack $request_stack) { - $this->requestMatcher = $request_matcher; - $this->routeProvider = $route_provider; - $this->requestStack = $request_stack; + public function __construct(AccessAwareRouterInterface $access_aware_router, UrlMatcherInterface $access_unaware_router, AccountInterface $account, InboundPathProcessorInterface $path_processor) { + $this->accessAwareRouter = $access_aware_router; + $this->accessUnawareRouter = $access_unaware_router; + $this->account = $account; + $this->pathProcessor = $path_processor; } /** * {@inheritdoc} */ public function isValid($path) { - // External URLs and the front page are always valid. - if ($path == '' || UrlHelper::isExternal($path)) { - return TRUE; + return (bool) $this->getUrlIfValid($path); + } + + /** + * {@inheritdoc} + */ + public function getUrlIfValid($path) { + $parsed_url = UrlHelper::parse($path); + + $options = []; + if (!empty($parsed_url['query'])) { + $options['query'] = $parsed_url['query']; + } + if (!empty($parsed_url['fragment'])) { + $options['fragment'] = $parsed_url['fragment']; } - // Check the routing system. - $collection = $this->routeProvider->getRoutesByPattern('/' . $path); - if ($collection->count() == 0) { + if ($parsed_url['path'] == '') { + return new Url('', [], $options); + } + elseif (UrlHelper::isExternal($path) && UrlHelper::isValid($path)) { + if (empty($parsed_url['path'])) { + return FALSE; + } + return Url::createFromPath($path); + } + + $request = Request::create('/' . $path); + $attributes = $this->getPathAttributes($path, $request); + + if (!$attributes) { return FALSE; } - // We can not use $this->requestMatcher->match() because we need to set - // the _menu_admin attribute to indicate a menu administrator is running - // the menu access check. - $request = RequestHelper::duplicate($this->requestStack->getCurrentRequest(), '/' . $path); - $request->attributes->set('_system_path', $path); - $request->attributes->set('_menu_admin', TRUE); + $route_name = $attributes[RouteObjectInterface::ROUTE_NAME]; + $route_parameters = $attributes['_raw_variables']->all(); + + return new Url($route_name, $route_parameters, $options + ['query' => $request->query->all()]); + } + + /** + * Gets the matched attributes for a given path. + * + * @param string $path + * The path to check. + * @param \Symfony\Component\HttpFoundation\Request $request + * A request object with the given path. + * + * @return array|bool + * An array of request attributes of FALSE if an exception was thrown. + */ + protected function getPathAttributes($path, Request $request) { + if ($this->account->hasPermission('link to any page')) { + $router = $this->accessUnawareRouter; + } + else { + $router = $this->accessAwareRouter; + } + + $path = $this->pathProcessor->processInbound($path, $request); try { - $this->requestMatcher->matchRequest($request); + return $router->match('/' . $path); + } + catch (ResourceNotFoundException $e) { + return FALSE; } catch (ParamNotConvertedException $e) { return FALSE; @@ -88,7 +148,6 @@ public function isValid($path) { catch (AccessDeniedHttpException $e) { return FALSE; } - return TRUE; } } diff --git a/core/lib/Drupal/Core/Path/PathValidatorInterface.php b/core/lib/Drupal/Core/Path/PathValidatorInterface.php index fed2f75..e0bcf04 100644 --- a/core/lib/Drupal/Core/Path/PathValidatorInterface.php +++ b/core/lib/Drupal/Core/Path/PathValidatorInterface.php @@ -2,7 +2,7 @@ /** * @file - * Contains Drupal\Core\Path\PathValidatorInterface + * Contains \Drupal\Core\Path\PathValidatorInterface */ namespace Drupal\Core\Path; @@ -13,6 +13,17 @@ interface PathValidatorInterface { /** + * Returns an URL object, if the path is valid and accessible. + * + * @param string $path + * The path to check. + * + * @return \Drupal\Core\Url|false + * The url object, or FALSE if the path is not valid. + */ + public function getUrlIfValid($path); + + /** * Checks if the URL path is valid and accessible by the current user. * * @param string $path diff --git a/core/lib/Drupal/Core/PathProcessor/InboundPathProcessorInterface.php b/core/lib/Drupal/Core/PathProcessor/InboundPathProcessorInterface.php index 946b29c..7bc22a8 100644 --- a/core/lib/Drupal/Core/PathProcessor/InboundPathProcessorInterface.php +++ b/core/lib/Drupal/Core/PathProcessor/InboundPathProcessorInterface.php @@ -22,6 +22,9 @@ * * @param \Symfony\Component\HttpFoundation\Request $request * The HttpRequest object representing the current request. + * + * @return string + * The processed path. */ public function processInbound($path, Request $request); diff --git a/core/lib/Drupal/Core/Url.php b/core/lib/Drupal/Core/Url.php index c6a8641..ea407cc 100644 --- a/core/lib/Drupal/Core/Url.php +++ b/core/lib/Drupal/Core/Url.php @@ -9,11 +9,9 @@ use Drupal\Component\Utility\UrlHelper; use Drupal\Core\DependencyInjection\DependencySerializationTrait; -use Drupal\Core\Routing\MatchingRouteNotFoundException; use Drupal\Core\Routing\UrlGeneratorInterface; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Routing\Exception\ResourceNotFoundException; /** * Defines an object that holds information about a URL. @@ -97,7 +95,15 @@ public function __construct($route_name, $route_parameters = array(), $options = } /** - * Returns the Url object matching a path. + * Returns the Url object matching a path. READ THE FOLLOWING SECURITY NOTE. + * + * SECURITY NOTE: The path is not checked to be valid and accessible by the + * current user to allow storing and reusing Url objects by different users. + * The 'path.validator' service getUrlIfValid() method should be used instead + * of this one if validation and access check is desired. Otherwise, + * 'access_manager' service checkNamedRoute() method should be used on the + * router name and parameters stored in the Url object returned by this + * method. * * @param string $path * A path (e.g. 'node/1', 'http://drupal.org'). @@ -118,27 +124,15 @@ public static function createFromPath($path) { // Special case the front page route. if ($path == '') { - $route_name = $path; - $route_parameters = array(); + return new static($path); } else { - // Look up the route name and parameters used for the given path. - try { - // We use the router without access checks because URL objects might be - // created and stored for different users. - $result = \Drupal::service('router.no_access_checks')->match('/' . $path); - } - catch (ResourceNotFoundException $e) { - throw new MatchingRouteNotFoundException(sprintf('No matching route could be found for the path "%s"', $path), 0, $e); - } - $route_name = $result[RouteObjectInterface::ROUTE_NAME]; - $route_parameters = $result['_raw_variables']->all(); + return static::createFromRequest(Request::create("/$path")); } - return new static($route_name, $route_parameters); } /** - * Returns the Url object matching a request. + * Returns the Url object matching a request. READ THE SECURITY NOTE ON createFromPath(). * * @param \Symfony\Component\HttpFoundation\Request $request * A request object. @@ -152,14 +146,9 @@ public static function createFromPath($path) { * Thrown when the request cannot be matched. */ public static function createFromRequest(Request $request) { - try { - // We use the router without access checks because URL objects might be - // created and stored for different users. - $result = \Drupal::service('router.no_access_checks')->matchRequest($request); - } - catch (ResourceNotFoundException $e) { - throw new MatchingRouteNotFoundException(sprintf('No matching route could be found for the request: %s', $request), 0, $e); - } + // We use the router without access checks because URL objects might be + // created and stored for different users. + $result = \Drupal::service('router.no_access_checks')->matchRequest($request); $route_name = $result[RouteObjectInterface::ROUTE_NAME]; $route_parameters = $result['_raw_variables']->all(); return new static($route_name, $route_parameters); diff --git a/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php b/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php index 040123b..cb95532 100644 --- a/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php +++ b/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php @@ -7,15 +7,10 @@ namespace Drupal\link\Plugin\Field\FieldWidget; -use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\ParamConverter\ParamNotConvertedException; -use Drupal\Core\Routing\MatchingRouteNotFoundException; -use Drupal\Core\Url; use Drupal\link\LinkItemInterface; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Plugin implementation of the 'link' widget. @@ -47,9 +42,10 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen $default_url_value = NULL; if (isset($items[$delta]->url)) { - $url = Url::createFromPath($items[$delta]->url); - $url->setOptions($items[$delta]->options); - $default_url_value = ltrim($url->toString(), '/'); + if ($url = \Drupal::pathValidator()->getUrlIfValid($items[$delta]->url)) { + $url->setOptions($items[$delta]->options); + $default_url_value = ltrim($url->toString(), '/'); + } } $element['url'] = array( '#type' => 'url', @@ -204,32 +200,16 @@ public function validateTitle(&$element, FormStateInterface $form_state, $form) public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { foreach ($values as &$value) { if (!empty($value['url'])) { - try { - $parsed_url = UrlHelper::parse($value['url']); - - // If internal links are supported, look up whether the given value is - // a path alias and store the system path instead. - if ($this->supportsInternalLinks() && !UrlHelper::isExternal($value['url'])) { - $parsed_url['path'] = \Drupal::service('path.alias_manager')->getPathByAlias($parsed_url['path']); - } - - $url = Url::createFromPath($parsed_url['path']); - $url->setOption('query', $parsed_url['query']); - $url->setOption('fragment', $parsed_url['fragment']); - $url->setOption('attributes', $value['attributes']); - - $value += $url->toArray(); - // Reset the URL value to contain only the path. - $value['url'] = $parsed_url['path']; + $url = \Drupal::pathValidator()->getUrlIfValid($value['url']); + if (!$url) { + return $values; } - catch (NotFoundHttpException $e) { - // Nothing to do here, LinkTypeConstraintValidator emits errors. - } - catch (MatchingRouteNotFoundException $e) { - // Nothing to do here, LinkTypeConstraintValidator emits errors. - } - catch (ParamNotConvertedException $e) { - // Nothing to do here, LinkTypeConstraintValidator emits errors. + + $value += $url->toArray(); + + // Reset the URL value to contain only the path. + if (!$url->isExternal() && $this->supportsInternalLinks()) { + $value['url'] = substr($url->toString(), strlen(\Drupal::request()->getBasePath() . '/')); } } } diff --git a/core/modules/link/src/Plugin/Validation/Constraint/LinkTypeConstraint.php b/core/modules/link/src/Plugin/Validation/Constraint/LinkTypeConstraint.php index de28bab..89ff6da 100644 --- a/core/modules/link/src/Plugin/Validation/Constraint/LinkTypeConstraint.php +++ b/core/modules/link/src/Plugin/Validation/Constraint/LinkTypeConstraint.php @@ -8,14 +8,9 @@ namespace Drupal\link\Plugin\Validation\Constraint; use Drupal\link\LinkItemInterface; -use Drupal\Core\Url; -use Drupal\Core\Routing\MatchingRouteNotFoundException; -use Drupal\Core\ParamConverter\ParamNotConvertedException; -use Drupal\Component\Utility\UrlHelper; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidatorInterface; use Symfony\Component\Validator\ExecutionContextInterface; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Validation constraint for links receiving data allowed by its settings. @@ -53,35 +48,19 @@ public function validatedBy() { */ public function validate($value, Constraint $constraint) { if (isset($value)) { - $url_is_valid = TRUE; + $url_is_valid = FALSE; /** @var $link_item \Drupal\link\LinkItemInterface */ $link_item = $value; $link_type = $link_item->getFieldDefinition()->getSetting('link_type'); $url_string = $link_item->url; // Validate the url property. if ($url_string !== '') { - try { - // @todo This shouldn't be needed, but massageFormValues() may not - // run. - $parsed_url = UrlHelper::parse($url_string); + if ($url = \Drupal::pathValidator()->getUrlIfValid($url_string)) { + $url_is_valid = (bool) $url; - $url = Url::createFromPath($parsed_url['path']); - - if ($url->isExternal() && !UrlHelper::isValid($url_string, TRUE)) { + if ($url->isExternal() && !($link_type & LinkItemInterface::LINK_EXTERNAL)) { $url_is_valid = FALSE; } - elseif ($url->isExternal() && !($link_type & LinkItemInterface::LINK_EXTERNAL)) { - $url_is_valid = FALSE; - } - } - catch (NotFoundHttpException $e) { - $url_is_valid = FALSE; - } - catch (MatchingRouteNotFoundException $e) { - $url_is_valid = FALSE; - } - catch (ParamNotConvertedException $e) { - $url_is_valid = FALSE; } } if (!$url_is_valid) { diff --git a/core/modules/link/src/Tests/LinkFieldTest.php b/core/modules/link/src/Tests/LinkFieldTest.php index 6729e6b..c7eb596 100644 --- a/core/modules/link/src/Tests/LinkFieldTest.php +++ b/core/modules/link/src/Tests/LinkFieldTest.php @@ -52,6 +52,7 @@ protected function setUp() { $this->web_user = $this->drupalCreateUser(array( 'view test entity', 'administer entity_test content', + 'link to any page', )); $this->drupalLogin($this->web_user); } diff --git a/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php b/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php index b3966eb..ae4bac0 100644 --- a/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php +++ b/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php @@ -18,11 +18,9 @@ use Drupal\Core\Menu\Form\MenuLinkFormInterface; use Drupal\Core\Menu\MenuLinkInterface; use Drupal\Core\Menu\MenuParentFormSelectorInterface; -use Drupal\Core\ParamConverter\ParamNotConvertedException; use Drupal\Core\Path\AliasManagerInterface; -use Drupal\Core\Routing\MatchingRouteNotFoundException; +use Drupal\Core\Path\PathValidatorInterface; use Drupal\Core\Session\AccountInterface; -use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Routing\RequestContext; @@ -78,6 +76,13 @@ class MenuLinkContentForm extends ContentEntityForm implements MenuLinkFormInter protected $account; /** + * The path validator. + * + * @var \Drupal\Core\Path\PathValidatorInterface + */ + protected $pathValidator; + + /** * Constructs a MenuLinkContentForm object. * * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager @@ -96,8 +101,10 @@ class MenuLinkContentForm extends ContentEntityForm implements MenuLinkFormInter * The access manager. * @param \Drupal\Core\Session\AccountInterface $account * The current user. + * @param \Drupal\Core\Path\PathValidatorInterface $path_validator + * The path validator. */ - public function __construct(EntityManagerInterface $entity_manager, MenuParentFormSelectorInterface $menu_parent_selector, AliasManagerInterface $alias_manager, ModuleHandlerInterface $module_handler, RequestContext $request_context, LanguageManagerInterface $language_manager, AccessManagerInterface $access_manager, AccountInterface $account) { + public function __construct(EntityManagerInterface $entity_manager, MenuParentFormSelectorInterface $menu_parent_selector, AliasManagerInterface $alias_manager, ModuleHandlerInterface $module_handler, RequestContext $request_context, LanguageManagerInterface $language_manager, AccessManagerInterface $access_manager, AccountInterface $account, PathValidatorInterface $path_validator) { parent::__construct($entity_manager, $language_manager); $this->menuParentSelector = $menu_parent_selector; $this->pathAliasManager = $alias_manager; @@ -106,6 +113,7 @@ public function __construct(EntityManagerInterface $entity_manager, MenuParentFo $this->languageManager = $language_manager; $this->accessManager = $access_manager; $this->account = $account; + $this->pathValidator = $path_validator; } /** @@ -120,7 +128,8 @@ public static function create(ContainerInterface $container) { $container->get('router.request_context'), $container->get('language_manager'), $container->get('access_manager'), - $container->get('current_user') + $container->get('current_user'), + $container->get('path.validator') ); } @@ -168,46 +177,6 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s } /** - * Breaks up a user-entered URL or path into all the relevant parts. - * - * @param string $url - * The user-entered URL or path. - * - * @return array - * The extracted parts. - */ - protected function extractUrl($url) { - $extracted = UrlHelper::parse($url); - $external = UrlHelper::isExternal($url); - if ($external) { - $extracted['url'] = $extracted['path']; - $extracted['route_name'] = NULL; - $extracted['route_parameters'] = array(); - } - else { - $extracted['url'] = ''; - // If the path doesn't match a Drupal path, the route should end up empty. - $extracted['route_name'] = NULL; - $extracted['route_parameters'] = array(); - try { - // Find the route_name. - $normal_path = $this->pathAliasManager->getPathByAlias($extracted['path']); - $url_obj = Url::createFromPath($normal_path); - $extracted['route_name'] = $url_obj->getRouteName(); - $extracted['route_parameters'] = $url_obj->getRouteParameters(); - } - catch (MatchingRouteNotFoundException $e) { - // The path doesn't match a Drupal path. - } - catch (ParamNotConvertedException $e) { - // A path like node/99 matched a route, but the route parameter was - // invalid (e.g. node with ID 99 does not exist). - } - } - return $extracted; - } - - /** * {@inheritdoc} */ public function extractFormValues(array &$form, FormStateInterface $form_state) { @@ -220,17 +189,24 @@ public function extractFormValues(array &$form, FormStateInterface $form_state) } $new_definition['parent'] = isset($parent) ? $parent : ''; - $extracted = $this->extractUrl($form_state->getValue('url')); - $new_definition['url'] = $extracted['url']; - $new_definition['route_name'] = $extracted['route_name']; - $new_definition['route_parameters'] = $extracted['route_parameters']; - $new_definition['options'] = array(); - if ($extracted['query']) { - $new_definition['options']['query'] = $extracted['query']; - } - if ($extracted['fragment']) { - $new_definition['options']['fragment'] = $extracted['fragment']; + $new_definition['url'] = NULL; + $new_definition['route_name'] = NULL; + $new_definition['route_parameters'] = []; + $new_definition['options'] = []; + + $extracted = $this->pathValidator->getUrlIfValid($form_state->getValue('url')); + + if ($extracted) { + if ($extracted->isExternal()) { + $new_definition['url'] = $extracted->getPath(); + } + else { + $new_definition['route_name'] = $extracted->getRouteName(); + $new_definition['route_parameters'] = $extracted->getRouteParameters(); + $new_definition['options'] = $extracted->getOptions(); + } } + $new_definition['title'] = $form_state->getValue(array('title', 0, 'value')); $new_definition['description'] = $form_state->getValue(array('description', 0, 'value')); $new_definition['weight'] = (int) $form_state->getValue(array('weight', 0, 'value')); @@ -380,31 +356,11 @@ public function save(array $form, FormStateInterface $form_state) { * The current state of the form. */ protected function doValidate(array $form, FormStateInterface $form_state) { - $extracted = $this->extractUrl($form_state->getValue('url')); + $extracted = $this->pathValidator->getUrlIfValid($form_state->getValue('url')); - // If both URL and route_name are empty, the entered value is not valid. - $valid = FALSE; - if ($extracted['url']) { - // This is an external link. - $valid = TRUE; - } - elseif ($extracted['route_name']) { - // Users are not allowed to add a link to a page they cannot access. - $valid = $this->accessManager->checkNamedRoute($extracted['route_name'], $extracted['route_parameters'], $this->account); - } - if (!$valid) { + if (!$extracted) { $form_state->setErrorByName('url', $this->t("The path '@link_path' is either invalid or you do not have access to it.", array('@link_path' => $form_state->getValue('url')))); } - elseif ($extracted['route_name']) { - // The user entered a Drupal path. - $normal_path = $this->pathAliasManager->getPathByAlias($extracted['path']); - if ($extracted['path'] != $normal_path) { - drupal_set_message($this->t('The menu system stores system paths only, but will use the URL alias for display. %link_path has been stored as %normal_path', array( - '%link_path' => $extracted['path'], - '%normal_path' => $normal_path, - ))); - } - } } } diff --git a/core/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module index 6f446c1..b705ebf 100644 --- a/core/modules/shortcut/shortcut.module +++ b/core/modules/shortcut/shortcut.module @@ -259,29 +259,6 @@ function shortcut_set_title_exists($title) { } /** - * Determines if a path corresponds to a valid shortcut link. - * - * @param string $path - * The path to the link. - * - * @return bool - * TRUE if the shortcut link is valid, FALSE otherwise. Valid links are ones - * that correspond to actual paths on the site. - * - * @see menu_edit_item_validate() - */ -function shortcut_valid_link($path) { - // Do not use URL aliases. - $normal_path = \Drupal::service('path.alias_manager')->getPathByAlias($path); - if ($path != $normal_path) { - $path = $normal_path; - } - - // An empty path is valid too and will be converted to . - return (!UrlHelper::isExternal($path) && (\Drupal::service('router.route_provider')->getRoutesByPattern('/' . $path)->count() > 0)) || empty($path) || $path == ''; -} - -/** * Returns an array of shortcut links, suitable for rendering. * * @param \Drupal\shortcut\ShortcutSetInterface $shortcut_set diff --git a/core/modules/shortcut/src/Controller/ShortcutSetController.php b/core/modules/shortcut/src/Controller/ShortcutSetController.php index e63e027..729430f 100644 --- a/core/modules/shortcut/src/Controller/ShortcutSetController.php +++ b/core/modules/shortcut/src/Controller/ShortcutSetController.php @@ -8,6 +8,7 @@ namespace Drupal\shortcut\Controller; use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Path\PathValidatorInterface; use Drupal\shortcut\ShortcutSetInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -20,6 +21,30 @@ class ShortcutSetController extends ControllerBase { /** + * The path validator. + * + * @var \Drupal\Core\Path\PathValidatorInterface + */ + protected $pathValidator; + + /** + * Creates a new ShortcutSetController instance. + * + * @param \Drupal\Core\Path\PathValidatorInterface $path_validator + * The path validator. + */ + public function __construct(PathValidatorInterface $path_validator) { + $this->pathValidator = $path_validator; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static($container->get('path.validator')); + } + + /** * Creates a new link in the provided shortcut set. * * @param \Drupal\shortcut\ShortcutSetInterface $shortcut_set @@ -35,7 +60,7 @@ class ShortcutSetController extends ControllerBase { public function addShortcutLinkInline(ShortcutSetInterface $shortcut_set, Request $request) { $link = $request->query->get('link'); $name = $request->query->get('name'); - if (shortcut_valid_link($link)) { + if ($this->pathValidator->isValid($link)) { $shortcut = $this->entityManager()->getStorage('shortcut')->create(array( 'title' => $name, 'shortcut_set' => $shortcut_set->id(), diff --git a/core/modules/shortcut/src/Entity/Shortcut.php b/core/modules/shortcut/src/Entity/Shortcut.php index 75541b8..e6fcf49 100644 --- a/core/modules/shortcut/src/Entity/Shortcut.php +++ b/core/modules/shortcut/src/Entity/Shortcut.php @@ -84,6 +84,13 @@ public function setWeight($weight) { /** * {@inheritdoc} */ + public function getUrl() { + return new Url($this->getRouteName(), $this->getRouteParams()); + } + + /** + * {@inheritdoc} + */ public function getRouteName() { return $this->get('route_name')->value; } diff --git a/core/modules/shortcut/src/ShortcutForm.php b/core/modules/shortcut/src/ShortcutForm.php index 1c70d52..c9f5d3d 100644 --- a/core/modules/shortcut/src/ShortcutForm.php +++ b/core/modules/shortcut/src/ShortcutForm.php @@ -8,8 +8,11 @@ namespace Drupal\shortcut; use Drupal\Core\Entity\ContentEntityForm; +use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Path\PathValidatorInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Form controller for the shortcut entity forms. @@ -24,6 +27,34 @@ class ShortcutForm extends ContentEntityForm { protected $entity; /** + * The path validator. + * + * @var \Drupal\Core\Path\PathValidatorInterface + */ + protected $pathValidator; + + /** + * Constructs a new ShortcutForm instance. + * + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager + * The entity manager + * @param \Drupal\Core\Path\PathValidatorInterface $path_validator + * The path validator. + */ + public function __construct(EntityManagerInterface $entity_manager, PathValidatorInterface $path_validator) { + parent::__construct($entity_manager); + + $this->pathValidator = $path_validator; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static($container->get('entity.manager'), $container->get('path.validator')); + } + + /** * {@inheritdoc} */ public function form(array $form, FormStateInterface $form_state) { @@ -65,7 +96,7 @@ public function buildEntity(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function validate(array $form, FormStateInterface $form_state) { - if (!shortcut_valid_link($form_state->getValue('path'))) { + if (!$this->pathValidator->isValid($form_state->getValue('path'))) { $form_state->setErrorByName('path', $this->t('The shortcut must correspond to a valid path on the site.')); } diff --git a/core/modules/shortcut/src/ShortcutInterface.php b/core/modules/shortcut/src/ShortcutInterface.php index 6dc0be2..90d90f9 100644 --- a/core/modules/shortcut/src/ShortcutInterface.php +++ b/core/modules/shortcut/src/ShortcutInterface.php @@ -53,6 +53,14 @@ public function getWeight(); public function setWeight($weight); /** + * Returns the URL object pointing to the configured route. + * + * @return \Drupal\Core\Url + * The URL object. + */ + public function getUrl(); + + /** * Returns the route name associated with this shortcut, if any. * * @return string|null diff --git a/core/modules/shortcut/src/Tests/ShortcutLinksTest.php b/core/modules/shortcut/src/Tests/ShortcutLinksTest.php index e13b4eb..1dc314d 100644 --- a/core/modules/shortcut/src/Tests/ShortcutLinksTest.php +++ b/core/modules/shortcut/src/Tests/ShortcutLinksTest.php @@ -38,13 +38,13 @@ public function testShortcutLinkAdd() { // Create some paths to test. $test_cases = array( - array('path' => ''), - array('path' => 'admin'), - array('path' => 'admin/config/system/site-information'), - array('path' => 'node/' . $this->node->id() . '/edit'), - array('path' => $path['alias']), - array('path' => 'router_test/test2'), - array('path' => 'router_test/test3/value'), + array('path' => '', 'route_name' => ''), + array('path' => 'admin', 'route_name' => 'system.admin'), + array('path' => 'admin/config/system/site-information', 'route_name' => 'system.site_information_settings'), + array('path' => 'node/' . $this->node->id() . '/edit', 'route_name' => 'entity.node.edit_form'), + array('path' => $path['alias'], 'route_name' => 'entity.node.canonical'), + array('path' => 'router_test/test2', 'route_name' => 'router_test.2'), + array('path' => 'router_test/test3/value', 'route_name' => 'router_test.3'), ); // Check that each new shortcut links where it should. @@ -57,8 +57,8 @@ public function testShortcutLinkAdd() { $this->drupalPostForm('admin/config/user-interface/shortcut/manage/' . $set->id() . '/add-link', $form_data, t('Save')); $this->assertResponse(200); $saved_set = ShortcutSet::load($set->id()); - $paths = $this->getShortcutInformation($saved_set, 'path'); - $this->assertTrue(in_array($this->container->get('path.alias_manager')->getPathByAlias($test['path']), $paths), 'Shortcut created: ' . $test['path']); + $routes = $this->getShortcutInformation($saved_set, 'route_name'); + $this->assertTrue(in_array($test['route_name'], $routes), 'Shortcut created: ' . $test['path']); $this->assertLink($title, 0, 'Shortcut link found on the page.'); } $saved_set = ShortcutSet::load($set->id()); @@ -73,6 +73,25 @@ public function testShortcutLinkAdd() { $this->assertEqual($entity->get('route_parameters')->first()->getValue(), $loaded->get('route_parameters')->first()->getValue()); } } + + // Login as non admin user, to check that access is checked when creating + // shortcuts. + $this->drupalLogin($this->shortcut_user); + $title = $this->randomMachineName(); + $form_data = [ + 'title[0][value]' => $title, + 'path' => 'admin', + ]; + $this->drupalPostForm('admin/config/user-interface/shortcut/manage/' . $set->id() . '/add-link', $form_data, t('Save')); + $this->assertResponse(200); + $this->assertRaw(t('The shortcut must correspond to a valid path on the site.')); + + $form_data = [ + 'title[0][value]' => $title, + 'path' => 'node', + ]; + $this->drupalPostForm('admin/config/user-interface/shortcut/manage/' . $set->id() . '/add-link', $form_data, t('Save')); + $this->assertLink($title, 0, 'Shortcut link found on the page.'); } /** @@ -137,8 +156,8 @@ public function testShortcutLinkChangePath() { $shortcut = reset($shortcuts); $this->drupalPostForm('admin/config/user-interface/shortcut/link/' . $shortcut->id(), array('title[0][value]' => $shortcut->getTitle(), 'path' => $new_link_path), t('Save')); $saved_set = ShortcutSet::load($set->id()); - $paths = $this->getShortcutInformation($saved_set, 'path'); - $this->assertTrue(in_array($new_link_path, $paths), 'Shortcut path changed: ' . $new_link_path); + $routes = $this->getShortcutInformation($saved_set, 'route_name'); + $this->assertTrue(in_array('system.admin_config', $routes), 'Shortcut path changed: ' . $new_link_path); $this->assertLinkByHref($new_link_path, 0, 'Shortcut with new path appears on the page.'); } diff --git a/core/modules/shortcut/src/Tests/ShortcutTestBase.php b/core/modules/shortcut/src/Tests/ShortcutTestBase.php index a61f80e..388f5d2 100644 --- a/core/modules/shortcut/src/Tests/ShortcutTestBase.php +++ b/core/modules/shortcut/src/Tests/ShortcutTestBase.php @@ -73,8 +73,8 @@ protected function setUp() { } // Create users. - $this->admin_user = $this->drupalCreateUser(array('access toolbar', 'administer shortcuts', 'view the administration theme', 'create article content', 'create page content', 'access content overview', 'administer users')); - $this->shortcut_user = $this->drupalCreateUser(array('customize shortcut links', 'switch shortcut sets')); + $this->admin_user = $this->drupalCreateUser(array('access toolbar', 'administer shortcuts', 'view the administration theme', 'create article content', 'create page content', 'access content overview', 'administer users', 'link to any page')); + $this->shortcut_user = $this->drupalCreateUser(array('customize shortcut links', 'switch shortcut sets', 'access shortcuts', 'access content')); // Create a node. $this->node = $this->drupalCreateNode(array('type' => 'article')); diff --git a/core/modules/system/system.module b/core/modules/system/system.module index dc372f8..c87b768 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -261,6 +261,11 @@ function system_permission() { 'title' => t('View site reports'), 'restrict access' => TRUE, ), + 'link to any page' => [ + 'title' => t('Link to any page'), + 'description' => t('This allows to bypass access checking when linking to internal paths.'), + 'restrict access' => TRUE, + ], ); } diff --git a/core/modules/user/src/Access/LoginStatusCheck.php b/core/modules/user/src/Access/LoginStatusCheck.php index fbceacd..7539fe2 100644 --- a/core/modules/user/src/Access/LoginStatusCheck.php +++ b/core/modules/user/src/Access/LoginStatusCheck.php @@ -9,7 +9,6 @@ use Drupal\Core\Routing\Access\AccessInterface; use Drupal\Core\Session\AccountInterface; -use Symfony\Component\HttpFoundation\Request; /** * Determines access to routes based on login status of current user. @@ -19,16 +18,14 @@ class LoginStatusCheck implements AccessInterface { /** * Checks access. * - * @param \Symfony\Component\HttpFoundation\Request $request - * The request object. * @param \Drupal\Core\Session\AccountInterface $account * The currently logged in account. * * @return string * A \Drupal\Core\Access\AccessInterface constant value. */ - public function access(Request $request, AccountInterface $account) { - return ($request->attributes->get('_menu_admin') || $account->isAuthenticated()) ? static::ALLOW : static::DENY; + public function access(AccountInterface $account) { + return $account->isAuthenticated() ? static::ALLOW : static::DENY; } } diff --git a/core/modules/user/src/Access/RegisterAccessCheck.php b/core/modules/user/src/Access/RegisterAccessCheck.php index aec179b..aa9433f 100644 --- a/core/modules/user/src/Access/RegisterAccessCheck.php +++ b/core/modules/user/src/Access/RegisterAccessCheck.php @@ -9,7 +9,6 @@ use Drupal\Core\Routing\Access\AccessInterface; use Drupal\Core\Session\AccountInterface; -use Symfony\Component\HttpFoundation\Request; /** * Access check for user registration routes. @@ -19,15 +18,13 @@ class RegisterAccessCheck implements AccessInterface { /** * Checks access. * - * @param \Symfony\Component\HttpFoundation\Request $request - * The request object. * @param \Drupal\Core\Session\AccountInterface $account * The currently logged in account. * * @return string * A \Drupal\Core\Access\AccessInterface constant value. */ - public function access(Request $request, AccountInterface $account) { - return ($request->attributes->get('_menu_admin') || $account->isAnonymous()) && (\Drupal::config('user.settings')->get('register') != USER_REGISTER_ADMINISTRATORS_ONLY) ? static::ALLOW : static::DENY; + public function access(AccountInterface $account) { + return ($account->isAnonymous()) && (\Drupal::config('user.settings')->get('register') != USER_REGISTER_ADMINISTRATORS_ONLY) ? static::ALLOW : static::DENY; } } diff --git a/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php b/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php index 46232a5..12cbad5 100644 --- a/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php +++ b/core/tests/Drupal/Tests/Component/Utility/UrlHelperTest.php @@ -273,6 +273,22 @@ public static function providerTestParse() { ), ), array( + 'http://', + array( + 'path' => '', + 'query' => array(), + 'fragment' => '', + ), + ), + array( + 'https://', + array( + 'path' => '', + 'query' => array(), + 'fragment' => '', + ), + ), + array( '/my/path?destination=home#footer', array( 'path' => '/my/path', diff --git a/core/tests/Drupal/Tests/Core/DrupalTest.php b/core/tests/Drupal/Tests/Core/DrupalTest.php index 0491099..f618beb 100644 --- a/core/tests/Drupal/Tests/Core/DrupalTest.php +++ b/core/tests/Drupal/Tests/Core/DrupalTest.php @@ -320,6 +320,22 @@ public function testFormBuilder() { } /** + * Tests the menuTree() method. + */ + public function testMenuTree() { + $this->setMockContainerService('menu.link_tree'); + $this->assertNotNull(\Drupal::menuTree()); + } + + /** + * Tests the pathValidator() method. + */ + public function testPathValidator() { + $this->setMockContainerService('path.validator'); + $this->assertNotNull(\Drupal::pathValidator()); + } + + /** * Sets up a mock expectation for the container get() method. * * @param string $service_name diff --git a/core/tests/Drupal/Tests/Core/ExternalUrlTest.php b/core/tests/Drupal/Tests/Core/ExternalUrlTest.php index 3285a9d..57a51d4 100644 --- a/core/tests/Drupal/Tests/Core/ExternalUrlTest.php +++ b/core/tests/Drupal/Tests/Core/ExternalUrlTest.php @@ -76,15 +76,10 @@ public function testCreateFromPath() { * * @covers ::createFromRequest() * - * @expectedException \Drupal\Core\Routing\MatchingRouteNotFoundException - * @expectedExceptionMessage No matching route could be found for the request: request_as_a_string + * @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException */ public function testCreateFromRequest() { - // Mock the request in order to override the __toString() method. - $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); - $request->expects($this->once()) - ->method('__toString') - ->will($this->returnValue('request_as_a_string')); + $request = Request::create('/test-path'); $this->router->expects($this->once()) ->method('matchRequest') diff --git a/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php b/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php index 4309a4e..0080e7d 100644 --- a/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php +++ b/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php @@ -44,7 +44,7 @@ class DefaultMenuLinkTreeManipulatorsTest extends UnitTestCase { /** * The original menu tree build in mockTree(). * - * @var \Drupal\Tests\Core\Menu\MenuLinkMock[] + * @var \Drupal\Core\Menu\MenuLinkTreeElement[] */ protected $originalTree = array(); @@ -134,6 +134,7 @@ public function testGenerateIndexAndSort() { * Tests the checkAccess() tree manipulator. * * @covers ::checkAccess + * @covers ::menuLinkCheckAccess */ public function testCheckAccess() { // Those menu links that are non-external will have their access checks @@ -179,6 +180,31 @@ public function testCheckAccess() { } /** + * Tests checkAccess() tree manipulator with 'link to any page' permission. + * + * @covers ::checkAccess + * @covers ::menuLinkCheckAccess + */ + public function testCheckAccessWithLinkToAnyPagePermission() { + $this->mockTree(); + $this->currentUser->expects($this->exactly(8)) + ->method('hasPermission') + ->with('link to any page') + ->willReturn(TRUE); + + $this->mockTree(); + $this->defaultMenuTreeManipulators->checkAccess($this->originalTree); + + $this->assertTrue($this->originalTree[1]->access); + $this->assertTrue($this->originalTree[2]->access); + $this->assertTrue($this->originalTree[2]->subtree[3]->access); + $this->assertTrue($this->originalTree[2]->subtree[3]->subtree[4]->access); + $this->assertTrue($this->originalTree[5]->subtree[7]->access); + $this->assertTrue($this->originalTree[6]->access); + $this->assertTrue($this->originalTree[8]->access); + } + + /** * Tests the flatten() tree manipulator. * * @covers ::flatten diff --git a/core/tests/Drupal/Tests/Core/Path/PathValidatorTest.php b/core/tests/Drupal/Tests/Core/Path/PathValidatorTest.php new file mode 100644 index 0000000..fe704e2 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Path/PathValidatorTest.php @@ -0,0 +1,326 @@ +accessAwareRouter = $this->getMock('Drupal\Core\Routing\AccessAwareRouterInterface'); + $this->accessUnawareRouter = $this->getMock('Symfony\Component\Routing\Matcher\UrlMatcherInterface'); + $this->account = $this->getMock('Drupal\Core\Session\AccountInterface'); + $this->pathProcessor = $this->getMock('Drupal\Core\PathProcessor\InboundPathProcessorInterface'); + $this->pathValidator = new PathValidator($this->accessAwareRouter, $this->accessUnawareRouter, $this->account, $this->pathProcessor); + } + + /** + * Tests the isValid() method for the frontpage. + * + * @covers ::isValid + */ + public function testIsValidWithFrontpage() { + $this->accessAwareRouter->expects($this->never()) + ->method('match'); + + $this->assertTrue($this->pathValidator->isValid('')); + } + + /** + * Tests the isValid() method for an external URL. + * + * @covers ::isValid + */ + public function testIsValidWithExternalUrl() { + $this->accessAwareRouter->expects($this->never()) + ->method('match'); + + $this->assertTrue($this->pathValidator->isValid('https://drupal.org')); + } + + /** + * Tests the isValid() method with an invalid external URL. + */ + public function testIsValidWithInvalidExternalUrl() { + $this->accessAwareRouter->expects($this->never()) + ->method('match'); + + $this->assertFalse($this->pathValidator->isValid('http://')); + } + + /** + * Tests the isValid() method with a 'link to any page' permission. + * + * @covers ::isValid + */ + public function testIsValidWithLinkToAnyPageAccount() { + $this->account->expects($this->once()) + ->method('hasPermission') + ->with('link to any page') + ->willReturn(TRUE); + $this->accessAwareRouter->expects($this->never()) + ->method('match'); + $this->accessUnawareRouter->expects($this->once()) + ->method('match') + ->with('/test-path') + ->willReturn([RouteObjectInterface::ROUTE_NAME => 'test_route', '_raw_variables' => new ParameterBag(['key' => 'value'])]); + $this->pathProcessor->expects($this->once()) + ->method('processInbound') + ->willReturnArgument(0); + + + $this->assertTrue($this->pathValidator->isValid('test-path')); + } + + /** + * Tests the isValid() method without the 'link to any page' permission. + * + * @covers ::isValid + */ + public function testIsValidWithoutLinkToAnyPageAccount() { + $this->account->expects($this->once()) + ->method('hasPermission') + ->with('link to any page') + ->willReturn(FALSE); + $this->accessUnawareRouter->expects($this->never()) + ->method('match'); + $this->accessAwareRouter->expects($this->once()) + ->method('match') + ->with('/test-path') + ->willReturn([RouteObjectInterface::ROUTE_NAME => 'test_route', '_raw_variables' => new ParameterBag(['key' => 'value'])]); + $this->pathProcessor->expects($this->once()) + ->method('processInbound') + ->willReturnArgument(0); + + $this->assertTrue($this->pathValidator->isValid('test-path')); + } + + /** + * Tests the isValid() method with a path alias. + * + * @covers ::isValid + */ + public function testIsValidWithPathAlias() { + $this->account->expects($this->once()) + ->method('hasPermission') + ->with('link to any page') + ->willReturn(FALSE); + $this->accessUnawareRouter->expects($this->never()) + ->method('match'); + $this->accessAwareRouter->expects($this->once()) + ->method('match') + ->with('/test-path') + ->willReturn([RouteObjectInterface::ROUTE_NAME => 'test_route', '_raw_variables' => new ParameterBag(['key' => 'value'])]); + $this->pathProcessor->expects($this->once()) + ->method('processInbound') + ->with('path-alias', $this->anything()) + ->willReturn('test-path'); + + $this->assertTrue($this->pathValidator->isValid('path-alias')); + } + + /** + * Tests the isValid() method with a user without access to the path. + * + * @covers ::isValid + */ + public function testIsValidWithAccessDenied() { + $this->account->expects($this->once()) + ->method('hasPermission') + ->with('link to any page') + ->willReturn(FALSE); + $this->accessUnawareRouter->expects($this->never()) + ->method('match'); + $this->accessAwareRouter->expects($this->once()) + ->method('match') + ->with('/test-path') + ->willThrowException(new AccessDeniedHttpException()); + $this->pathProcessor->expects($this->once()) + ->method('processInbound') + ->willReturnArgument(0); + + $this->assertFalse($this->pathValidator->isValid('test-path')); + } + + /** + * Tests the isValid() method with a not working param converting. + * + * @covers ::isValid + */ + public function testIsValidWithFailingParameterConverting() { + $this->account->expects($this->once()) + ->method('hasPermission') + ->with('link to any page') + ->willReturn(FALSE); + $this->accessUnawareRouter->expects($this->never()) + ->method('match'); + $this->accessAwareRouter->expects($this->once()) + ->method('match') + ->with('/entity-test/1') + ->willThrowException(new ParamNotConvertedException()); + $this->pathProcessor->expects($this->once()) + ->method('processInbound') + ->willReturnArgument(0); + + $this->assertFalse($this->pathValidator->isValid('entity-test/1')); + } + + /** + * Tests the isValid() method with a not existing path. + * + * @covers ::isValid + */ + public function testIsValidWithNotExistingPath() { + $this->account->expects($this->once()) + ->method('hasPermission') + ->with('link to any page') + ->willReturn(FALSE); + $this->accessUnawareRouter->expects($this->never()) + ->method('match'); + $this->accessAwareRouter->expects($this->once()) + ->method('match') + ->with('/not-existing-path') + ->willThrowException(new ResourceNotFoundException()); + $this->pathProcessor->expects($this->once()) + ->method('processInbound') + ->willReturnArgument(0); + + $this->assertFalse($this->pathValidator->isValid('not-existing-path')); + } + + /** + * Tests the getUrlIfValid() method when there is access. + */ + public function testGetUrlIfValidWithAccess() { + $this->account->expects($this->once()) + ->method('hasPermission') + ->with('link to any page') + ->willReturn(FALSE); + + $this->accessAwareRouter->expects($this->once()) + ->method('match') + ->with('/test-path') + ->willReturn([RouteObjectInterface::ROUTE_NAME => 'test_route', '_raw_variables' => new ParameterBag(['key' => 'value'])]); + $this->pathProcessor->expects($this->once()) + ->method('processInbound') + ->willReturnArgument(0); + + $url = $this->pathValidator->getUrlIfValid('test-path'); + $this->assertInstanceOf('Drupal\Core\Url', $url); + + $this->assertEquals('test_route', $url->getRouteName()); + $this->assertEquals(['key' => 'value'], $url->getRouteParameters()); + } + + /** + * Tests the getUrlIfValid() method with a query in the path. + */ + public function testGetUrlIfValidWithQuery() { + $this->account->expects($this->once()) + ->method('hasPermission') + ->with('link to any page') + ->willReturn(FALSE); + + $this->accessAwareRouter->expects($this->once()) + ->method('match') + ->with('/test-path?k=bar') + ->willReturn([RouteObjectInterface::ROUTE_NAME => 'test_route', '_raw_variables' => new ParameterBag()]); + $this->pathProcessor->expects($this->once()) + ->method('processInbound') + ->willReturnArgument(0); + + $url = $this->pathValidator->getUrlIfValid('test-path?k=bar'); + $this->assertInstanceOf('Drupal\Core\Url', $url); + + $this->assertEquals('test_route', $url->getRouteName()); + $this->assertEquals(['k' => 'bar'], $url->getOptions()['query']); + } + + /** + * Tests the getUrlIfValid() method where there is no access. + */ + public function testGetUrlIfValidWithoutAccess() { + $this->account->expects($this->once()) + ->method('hasPermission') + ->with('link to any page') + ->willReturn(FALSE); + + $this->accessAwareRouter->expects($this->once()) + ->method('match') + ->with('/test-path') + ->willThrowException(new AccessDeniedHttpException()); + + $this->pathProcessor->expects($this->once()) + ->method('processInbound') + ->willReturnArgument(0); + + $url = $this->pathValidator->getUrlIfValid('test-path'); + $this->assertFalse($url); + } + + /** + * Tests the getUrlIfValid() method with a front page + query + fragments. + */ + public function testGetUrlIfValidWithFrontPageAndQueryAndFragments() { + $url = $this->pathValidator->getUrlIfValid('?hei=sen#berg'); + $this->assertEquals('', $url->getRouteName()); + $this->assertEquals(['hei' => 'sen'], $url->getOptions()['query']); + $this->assertEquals('berg', $url->getOptions()['fragment']); + } + +} + diff --git a/core/tests/Drupal/Tests/Core/UrlTest.php b/core/tests/Drupal/Tests/Core/UrlTest.php index 735af21..92f429d 100644 --- a/core/tests/Drupal/Tests/Core/UrlTest.php +++ b/core/tests/Drupal/Tests/Core/UrlTest.php @@ -72,22 +72,27 @@ protected function setUp() { * @covers ::createFromPath() */ public function testCreateFromPath() { - $this->router->expects($this->any()) - ->method('match') - ->will($this->returnValueMap(array( - array('/node', array( + $this->router->expects($this->at(0)) + ->method('matchRequest') + ->with(Request::create('/node')) + ->willReturn([ RouteObjectInterface::ROUTE_NAME => 'view.frontpage.page_1', '_raw_variables' => new ParameterBag(), - )), - array('/node/1', array( - RouteObjectInterface::ROUTE_NAME => 'node_view', - '_raw_variables' => new ParameterBag(array('node' => '1')), - )), - array('/node/2/edit', array( - RouteObjectInterface::ROUTE_NAME => 'node_edit', - '_raw_variables' => new ParameterBag(array('node' => '2')), - )), - ))); + ]); + $this->router->expects($this->at(1)) + ->method('matchRequest') + ->with(Request::create('/node/1')) + ->willReturn([ + RouteObjectInterface::ROUTE_NAME => 'node_view', + '_raw_variables' => new ParameterBag(['node' => '1']), + ]); + $this->router->expects($this->at(2)) + ->method('matchRequest') + ->with(Request::create('/node/2/edit')) + ->willReturn([ + RouteObjectInterface::ROUTE_NAME => 'node_edit', + '_raw_variables' => new ParameterBag(['node' => '2']), + ]); $urls = array(); foreach ($this->map as $index => $values) { @@ -114,13 +119,12 @@ public function testCreateFromPathFront() { * * @covers ::createFromPath() * - * @expectedException \Drupal\Core\Routing\MatchingRouteNotFoundException - * @expectedExceptionMessage No matching route could be found for the path "non-existent" + * @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException */ public function testCreateFromPathInvalid() { $this->router->expects($this->once()) - ->method('match') - ->with('/non-existent') + ->method('matchRequest') + ->with(Request::create('/non-existent')) ->will($this->throwException(new ResourceNotFoundException())); $this->assertNull(Url::createFromPath('non-existent')); @@ -155,15 +159,10 @@ public function testCreateFromRequest() { * * @covers ::createFromRequest() * - * @expectedException \Drupal\Core\Routing\MatchingRouteNotFoundException - * @expectedExceptionMessage No matching route could be found for the request: request_as_a_string + * @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException */ public function testCreateFromRequestInvalid() { - // Mock the request in order to override the __toString() method. - $request = $this->getMock('Symfony\Component\HttpFoundation\Request'); - $request->expects($this->once()) - ->method('__toString') - ->will($this->returnValue('request_as_a_string')); + $request = Request::create('/test-path'); $this->router->expects($this->once()) ->method('matchRequest')