Issue #2323721 by dawehner, chx: Fixed [sechole] Link field item and menu link information leakage.

8.0.x
webchick 2014-08-28 23:13:46 -07:00
parent aca1ec38eb
commit 68f576fe81
28 changed files with 723 additions and 283 deletions

View File

@ -506,7 +506,7 @@ services:
arguments: ['@config.factory'] arguments: ['@config.factory']
path.validator: path.validator:
class: Drupal\Core\Path\PathValidator 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 # The argument to the hashing service defined in services.yml, to the
# constructor of PhpassHashedPassword is the log2 number of iterations for # constructor of PhpassHashedPassword is the log2 number of iterations for

View File

@ -666,4 +666,13 @@ class Drupal {
return static::$container->get('menu.link_tree'); 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');
}
} }

View File

@ -151,7 +151,12 @@ class UrlHelper {
if (strpos($url, '://') !== FALSE) { if (strpos($url, '://') !== FALSE) {
// Split off everything before the query string into 'path'. // Split off everything before the query string into 'path'.
$parts = explode('?', $url); $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 there is a query string, transform it into keyed query parameters.
if (isset($parts[1])) { if (isset($parts[1])) {
$query_parts = explode('#', $parts[1]); $query_parts = explode('#', $parts[1]);

View File

@ -8,6 +8,7 @@
namespace Drupal\Core\Menu; namespace Drupal\Core\Menu;
use Drupal\Core\Access\AccessManagerInterface; use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Path\PathValidator;
use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AccountInterface;
/** /**
@ -93,6 +94,9 @@ class DefaultMenuLinkTreeManipulators {
* TRUE if the current user can access the link, FALSE otherwise. * TRUE if the current user can access the link, FALSE otherwise.
*/ */
protected function menuLinkCheckAccess(MenuLinkInterface $instance) { 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 // Use the definition here since that's a lot faster than creating a Url
// object that we don't need. // object that we don't need.
$definition = $instance->getPluginDefinition(); $definition = $instance->getPluginDefinition();

View File

@ -2,18 +2,22 @@
/** /**
* @file * @file
* Contains Drupal\Core\Path\PathValidator * Contains \Drupal\Core\Path\PathValidator
*/ */
namespace Drupal\Core\Path; namespace Drupal\Core\Path;
use Drupal\Component\Utility\UrlHelper; use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\ParamConverter\ParamNotConvertedException; use Drupal\Core\ParamConverter\ParamNotConvertedException;
use Drupal\Core\Routing\RequestHelper; use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Drupal\Core\Routing\RouteProviderInterface; use Drupal\Core\Routing\AccessAwareRouterInterface;
use Symfony\Component\HttpFoundation\RequestStack; 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\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. * Provides a default path validator and access checker.
@ -21,66 +25,122 @@ use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
class PathValidator implements PathValidatorInterface { 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. * Creates a new PathValidator.
* *
* @param \Symfony\Component\Routing\Matcher\RequestMatcherInterface $request_matcher * @param \Drupal\Core\Routing\AccessAwareRouterInterface $access_aware_router
* The request matcher. * The access aware router.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider * @param \Symfony\Component\Routing\Matcher\UrlMatcherInterface $access_unaware_router
* The route provider. * A router implementation which does not check access.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack * @param \Drupal\Core\Session\AccountInterface $account
* The request stack. * 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) { public function __construct(AccessAwareRouterInterface $access_aware_router, UrlMatcherInterface $access_unaware_router, AccountInterface $account, InboundPathProcessorInterface $path_processor) {
$this->requestMatcher = $request_matcher; $this->accessAwareRouter = $access_aware_router;
$this->routeProvider = $route_provider; $this->accessUnawareRouter = $access_unaware_router;
$this->requestStack = $request_stack; $this->account = $account;
$this->pathProcessor = $path_processor;
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function isValid($path) { public function isValid($path) {
// External URLs and the front page are always valid. return (bool) $this->getUrlIfValid($path);
if ($path == '<front>' || UrlHelper::isExternal($path)) { }
return TRUE;
/**
* {@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. if ($parsed_url['path'] == '<front>') {
$collection = $this->routeProvider->getRoutesByPattern('/' . $path); return new Url('<front>', [], $options);
if ($collection->count() == 0) { }
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; return FALSE;
} }
// We can not use $this->requestMatcher->match() because we need to set $route_name = $attributes[RouteObjectInterface::ROUTE_NAME];
// the _menu_admin attribute to indicate a menu administrator is running $route_parameters = $attributes['_raw_variables']->all();
// the menu access check.
$request = RequestHelper::duplicate($this->requestStack->getCurrentRequest(), '/' . $path); return new Url($route_name, $route_parameters, $options + ['query' => $request->query->all()]);
$request->attributes->set('_system_path', $path); }
$request->attributes->set('_menu_admin', TRUE);
/**
* 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 { try {
$this->requestMatcher->matchRequest($request); return $router->match('/' . $path);
}
catch (ResourceNotFoundException $e) {
return FALSE;
} }
catch (ParamNotConvertedException $e) { catch (ParamNotConvertedException $e) {
return FALSE; return FALSE;
@ -88,7 +148,6 @@ class PathValidator implements PathValidatorInterface {
catch (AccessDeniedHttpException $e) { catch (AccessDeniedHttpException $e) {
return FALSE; return FALSE;
} }
return TRUE;
} }
} }

View File

@ -2,7 +2,7 @@
/** /**
* @file * @file
* Contains Drupal\Core\Path\PathValidatorInterface * Contains \Drupal\Core\Path\PathValidatorInterface
*/ */
namespace Drupal\Core\Path; namespace Drupal\Core\Path;
@ -12,6 +12,17 @@ namespace Drupal\Core\Path;
*/ */
interface PathValidatorInterface { 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. * Checks if the URL path is valid and accessible by the current user.
* *

View File

@ -22,6 +22,9 @@ interface InboundPathProcessorInterface {
* *
* @param \Symfony\Component\HttpFoundation\Request $request * @param \Symfony\Component\HttpFoundation\Request $request
* The HttpRequest object representing the current request. * The HttpRequest object representing the current request.
*
* @return string
* The processed path.
*/ */
public function processInbound($path, Request $request); public function processInbound($path, Request $request);

View File

@ -9,11 +9,9 @@ namespace Drupal\Core;
use Drupal\Component\Utility\UrlHelper; use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Routing\MatchingRouteNotFoundException;
use Drupal\Core\Routing\UrlGeneratorInterface; use Drupal\Core\Routing\UrlGeneratorInterface;
use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
/** /**
* Defines an object that holds information about a URL. * Defines an object that holds information about a URL.
@ -97,7 +95,15 @@ class Url {
} }
/** /**
* 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 * @param string $path
* A path (e.g. 'node/1', 'http://drupal.org'). * A path (e.g. 'node/1', 'http://drupal.org').
@ -118,27 +124,15 @@ class Url {
// Special case the front page route. // Special case the front page route.
if ($path == '<front>') { if ($path == '<front>') {
$route_name = $path; return new static($path);
$route_parameters = array();
} }
else { else {
// Look up the route name and parameters used for the given path. return static::createFromRequest(Request::create("/$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 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 * @param \Symfony\Component\HttpFoundation\Request $request
* A request object. * A request object.
@ -152,14 +146,9 @@ class Url {
* Thrown when the request cannot be matched. * Thrown when the request cannot be matched.
*/ */
public static function createFromRequest(Request $request) { public static function createFromRequest(Request $request) {
try { // We use the router without access checks because URL objects might be
// We use the router without access checks because URL objects might be // created and stored for different users.
// created and stored for different users. $result = \Drupal::service('router.no_access_checks')->matchRequest($request);
$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);
}
$route_name = $result[RouteObjectInterface::ROUTE_NAME]; $route_name = $result[RouteObjectInterface::ROUTE_NAME];
$route_parameters = $result['_raw_variables']->all(); $route_parameters = $result['_raw_variables']->all();
return new static($route_name, $route_parameters); return new static($route_name, $route_parameters);

View File

@ -7,15 +7,10 @@
namespace Drupal\link\Plugin\Field\FieldWidget; namespace Drupal\link\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase; use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\ParamConverter\ParamNotConvertedException;
use Drupal\Core\Routing\MatchingRouteNotFoundException;
use Drupal\Core\Url;
use Drupal\link\LinkItemInterface; use Drupal\link\LinkItemInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/** /**
* Plugin implementation of the 'link' widget. * Plugin implementation of the 'link' widget.
@ -47,9 +42,10 @@ class LinkWidget extends WidgetBase {
$default_url_value = NULL; $default_url_value = NULL;
if (isset($items[$delta]->url)) { if (isset($items[$delta]->url)) {
$url = Url::createFromPath($items[$delta]->url); if ($url = \Drupal::pathValidator()->getUrlIfValid($items[$delta]->url)) {
$url->setOptions($items[$delta]->options); $url->setOptions($items[$delta]->options);
$default_url_value = ltrim($url->toString(), '/'); $default_url_value = ltrim($url->toString(), '/');
}
} }
$element['url'] = array( $element['url'] = array(
'#type' => 'url', '#type' => 'url',
@ -204,32 +200,16 @@ class LinkWidget extends WidgetBase {
public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
foreach ($values as &$value) { foreach ($values as &$value) {
if (!empty($value['url'])) { if (!empty($value['url'])) {
try { $url = \Drupal::pathValidator()->getUrlIfValid($value['url']);
$parsed_url = UrlHelper::parse($value['url']); if (!$url) {
return $values;
// 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'];
} }
catch (NotFoundHttpException $e) {
// Nothing to do here, LinkTypeConstraintValidator emits errors. $value += $url->toArray();
}
catch (MatchingRouteNotFoundException $e) { // Reset the URL value to contain only the path.
// Nothing to do here, LinkTypeConstraintValidator emits errors. if (!$url->isExternal() && $this->supportsInternalLinks()) {
} $value['url'] = substr($url->toString(), strlen(\Drupal::request()->getBasePath() . '/'));
catch (ParamNotConvertedException $e) {
// Nothing to do here, LinkTypeConstraintValidator emits errors.
} }
} }
} }

View File

@ -8,14 +8,9 @@
namespace Drupal\link\Plugin\Validation\Constraint; namespace Drupal\link\Plugin\Validation\Constraint;
use Drupal\link\LinkItemInterface; 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\Constraint;
use Symfony\Component\Validator\ConstraintValidatorInterface; use Symfony\Component\Validator\ConstraintValidatorInterface;
use Symfony\Component\Validator\ExecutionContextInterface; use Symfony\Component\Validator\ExecutionContextInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/** /**
* Validation constraint for links receiving data allowed by its settings. * Validation constraint for links receiving data allowed by its settings.
@ -53,35 +48,19 @@ class LinkTypeConstraint extends Constraint implements ConstraintValidatorInterf
*/ */
public function validate($value, Constraint $constraint) { public function validate($value, Constraint $constraint) {
if (isset($value)) { if (isset($value)) {
$url_is_valid = TRUE; $url_is_valid = FALSE;
/** @var $link_item \Drupal\link\LinkItemInterface */ /** @var $link_item \Drupal\link\LinkItemInterface */
$link_item = $value; $link_item = $value;
$link_type = $link_item->getFieldDefinition()->getSetting('link_type'); $link_type = $link_item->getFieldDefinition()->getSetting('link_type');
$url_string = $link_item->url; $url_string = $link_item->url;
// Validate the url property. // Validate the url property.
if ($url_string !== '') { if ($url_string !== '') {
try { if ($url = \Drupal::pathValidator()->getUrlIfValid($url_string)) {
// @todo This shouldn't be needed, but massageFormValues() may not $url_is_valid = (bool) $url;
// run.
$parsed_url = UrlHelper::parse($url_string);
$url = Url::createFromPath($parsed_url['path']); if ($url->isExternal() && !($link_type & LinkItemInterface::LINK_EXTERNAL)) {
if ($url->isExternal() && !UrlHelper::isValid($url_string, TRUE)) {
$url_is_valid = FALSE; $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) { if (!$url_is_valid) {

View File

@ -52,6 +52,7 @@ class LinkFieldTest extends WebTestBase {
$this->web_user = $this->drupalCreateUser(array( $this->web_user = $this->drupalCreateUser(array(
'view test entity', 'view test entity',
'administer entity_test content', 'administer entity_test content',
'link to any page',
)); ));
$this->drupalLogin($this->web_user); $this->drupalLogin($this->web_user);
} }

View File

@ -18,11 +18,9 @@ use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Menu\Form\MenuLinkFormInterface; use Drupal\Core\Menu\Form\MenuLinkFormInterface;
use Drupal\Core\Menu\MenuLinkInterface; use Drupal\Core\Menu\MenuLinkInterface;
use Drupal\Core\Menu\MenuParentFormSelectorInterface; use Drupal\Core\Menu\MenuParentFormSelectorInterface;
use Drupal\Core\ParamConverter\ParamNotConvertedException;
use Drupal\Core\Path\AliasManagerInterface; use Drupal\Core\Path\AliasManagerInterface;
use Drupal\Core\Routing\MatchingRouteNotFoundException; use Drupal\Core\Path\PathValidatorInterface;
use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\RequestContext;
@ -77,6 +75,13 @@ class MenuLinkContentForm extends ContentEntityForm implements MenuLinkFormInter
*/ */
protected $account; protected $account;
/**
* The path validator.
*
* @var \Drupal\Core\Path\PathValidatorInterface
*/
protected $pathValidator;
/** /**
* Constructs a MenuLinkContentForm object. * Constructs a MenuLinkContentForm object.
* *
@ -96,8 +101,10 @@ class MenuLinkContentForm extends ContentEntityForm implements MenuLinkFormInter
* The access manager. * The access manager.
* @param \Drupal\Core\Session\AccountInterface $account * @param \Drupal\Core\Session\AccountInterface $account
* The current user. * 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); parent::__construct($entity_manager, $language_manager);
$this->menuParentSelector = $menu_parent_selector; $this->menuParentSelector = $menu_parent_selector;
$this->pathAliasManager = $alias_manager; $this->pathAliasManager = $alias_manager;
@ -106,6 +113,7 @@ class MenuLinkContentForm extends ContentEntityForm implements MenuLinkFormInter
$this->languageManager = $language_manager; $this->languageManager = $language_manager;
$this->accessManager = $access_manager; $this->accessManager = $access_manager;
$this->account = $account; $this->account = $account;
$this->pathValidator = $path_validator;
} }
/** /**
@ -120,7 +128,8 @@ class MenuLinkContentForm extends ContentEntityForm implements MenuLinkFormInter
$container->get('router.request_context'), $container->get('router.request_context'),
$container->get('language_manager'), $container->get('language_manager'),
$container->get('access_manager'), $container->get('access_manager'),
$container->get('current_user') $container->get('current_user'),
$container->get('path.validator')
); );
} }
@ -167,46 +176,6 @@ class MenuLinkContentForm extends ContentEntityForm implements MenuLinkFormInter
$this->save($form, $form_state); $this->save($form, $form_state);
} }
/**
* 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} * {@inheritdoc}
*/ */
@ -220,17 +189,24 @@ class MenuLinkContentForm extends ContentEntityForm implements MenuLinkFormInter
} }
$new_definition['parent'] = isset($parent) ? $parent : ''; $new_definition['parent'] = isset($parent) ? $parent : '';
$extracted = $this->extractUrl($form_state->getValue('url')); $new_definition['url'] = NULL;
$new_definition['url'] = $extracted['url']; $new_definition['route_name'] = NULL;
$new_definition['route_name'] = $extracted['route_name']; $new_definition['route_parameters'] = [];
$new_definition['route_parameters'] = $extracted['route_parameters']; $new_definition['options'] = [];
$new_definition['options'] = array();
if ($extracted['query']) { $extracted = $this->pathValidator->getUrlIfValid($form_state->getValue('url'));
$new_definition['options']['query'] = $extracted['query'];
} if ($extracted) {
if ($extracted['fragment']) { if ($extracted->isExternal()) {
$new_definition['options']['fragment'] = $extracted['fragment']; $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['title'] = $form_state->getValue(array('title', 0, 'value'));
$new_definition['description'] = $form_state->getValue(array('description', 0, 'value')); $new_definition['description'] = $form_state->getValue(array('description', 0, 'value'));
$new_definition['weight'] = (int) $form_state->getValue(array('weight', 0, 'value')); $new_definition['weight'] = (int) $form_state->getValue(array('weight', 0, 'value'));
@ -380,31 +356,11 @@ class MenuLinkContentForm extends ContentEntityForm implements MenuLinkFormInter
* The current state of the form. * The current state of the form.
*/ */
protected function doValidate(array $form, FormStateInterface $form_state) { 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. if (!$extracted) {
$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) {
$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')))); $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,
)));
}
}
} }
} }

View File

@ -258,29 +258,6 @@ function shortcut_set_title_exists($title) {
return FALSE; return FALSE;
} }
/**
* 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 <front>.
return (!UrlHelper::isExternal($path) && (\Drupal::service('router.route_provider')->getRoutesByPattern('/' . $path)->count() > 0)) || empty($path) || $path == '<front>';
}
/** /**
* Returns an array of shortcut links, suitable for rendering. * Returns an array of shortcut links, suitable for rendering.
* *

View File

@ -8,6 +8,7 @@
namespace Drupal\shortcut\Controller; namespace Drupal\shortcut\Controller;
use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Path\PathValidatorInterface;
use Drupal\shortcut\ShortcutSetInterface; use Drupal\shortcut\ShortcutSetInterface;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\RedirectResponse;
@ -19,6 +20,30 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
*/ */
class ShortcutSetController extends ControllerBase { 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. * Creates a new link in the provided shortcut set.
* *
@ -35,7 +60,7 @@ class ShortcutSetController extends ControllerBase {
public function addShortcutLinkInline(ShortcutSetInterface $shortcut_set, Request $request) { public function addShortcutLinkInline(ShortcutSetInterface $shortcut_set, Request $request) {
$link = $request->query->get('link'); $link = $request->query->get('link');
$name = $request->query->get('name'); $name = $request->query->get('name');
if (shortcut_valid_link($link)) { if ($this->pathValidator->isValid($link)) {
$shortcut = $this->entityManager()->getStorage('shortcut')->create(array( $shortcut = $this->entityManager()->getStorage('shortcut')->create(array(
'title' => $name, 'title' => $name,
'shortcut_set' => $shortcut_set->id(), 'shortcut_set' => $shortcut_set->id(),

View File

@ -81,6 +81,13 @@ class Shortcut extends ContentEntityBase implements ShortcutInterface {
return $this; return $this;
} }
/**
* {@inheritdoc}
*/
public function getUrl() {
return new Url($this->getRouteName(), $this->getRouteParams());
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */

View File

@ -8,8 +8,11 @@
namespace Drupal\shortcut; namespace Drupal\shortcut;
use Drupal\Core\Entity\ContentEntityForm; use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Path\PathValidatorInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/** /**
* Form controller for the shortcut entity forms. * Form controller for the shortcut entity forms.
@ -23,6 +26,34 @@ class ShortcutForm extends ContentEntityForm {
*/ */
protected $entity; 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} * {@inheritdoc}
*/ */
@ -65,7 +96,7 @@ class ShortcutForm extends ContentEntityForm {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function validate(array $form, FormStateInterface $form_state) { 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.')); $form_state->setErrorByName('path', $this->t('The shortcut must correspond to a valid path on the site.'));
} }

View File

@ -52,6 +52,14 @@ interface ShortcutInterface extends ContentEntityInterface {
*/ */
public function setWeight($weight); 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. * Returns the route name associated with this shortcut, if any.
* *

View File

@ -38,13 +38,13 @@ class ShortcutLinksTest extends ShortcutTestBase {
// Create some paths to test. // Create some paths to test.
$test_cases = array( $test_cases = array(
array('path' => ''), array('path' => '<front>', 'route_name' => '<front>'),
array('path' => 'admin'), array('path' => 'admin', 'route_name' => 'system.admin'),
array('path' => 'admin/config/system/site-information'), array('path' => 'admin/config/system/site-information', 'route_name' => 'system.site_information_settings'),
array('path' => 'node/' . $this->node->id() . '/edit'), array('path' => 'node/' . $this->node->id() . '/edit', 'route_name' => 'entity.node.edit_form'),
array('path' => $path['alias']), array('path' => $path['alias'], 'route_name' => 'entity.node.canonical'),
array('path' => 'router_test/test2'), array('path' => 'router_test/test2', 'route_name' => 'router_test.2'),
array('path' => 'router_test/test3/value'), array('path' => 'router_test/test3/value', 'route_name' => 'router_test.3'),
); );
// Check that each new shortcut links where it should. // Check that each new shortcut links where it should.
@ -57,8 +57,8 @@ class ShortcutLinksTest extends ShortcutTestBase {
$this->drupalPostForm('admin/config/user-interface/shortcut/manage/' . $set->id() . '/add-link', $form_data, t('Save')); $this->drupalPostForm('admin/config/user-interface/shortcut/manage/' . $set->id() . '/add-link', $form_data, t('Save'));
$this->assertResponse(200); $this->assertResponse(200);
$saved_set = ShortcutSet::load($set->id()); $saved_set = ShortcutSet::load($set->id());
$paths = $this->getShortcutInformation($saved_set, 'path'); $routes = $this->getShortcutInformation($saved_set, 'route_name');
$this->assertTrue(in_array($this->container->get('path.alias_manager')->getPathByAlias($test['path']), $paths), 'Shortcut created: ' . $test['path']); $this->assertTrue(in_array($test['route_name'], $routes), 'Shortcut created: ' . $test['path']);
$this->assertLink($title, 0, 'Shortcut link found on the page.'); $this->assertLink($title, 0, 'Shortcut link found on the page.');
} }
$saved_set = ShortcutSet::load($set->id()); $saved_set = ShortcutSet::load($set->id());
@ -73,6 +73,25 @@ class ShortcutLinksTest extends ShortcutTestBase {
$this->assertEqual($entity->get('route_parameters')->first()->getValue(), $loaded->get('route_parameters')->first()->getValue()); $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 @@ class ShortcutLinksTest extends ShortcutTestBase {
$shortcut = reset($shortcuts); $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')); $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()); $saved_set = ShortcutSet::load($set->id());
$paths = $this->getShortcutInformation($saved_set, 'path'); $routes = $this->getShortcutInformation($saved_set, 'route_name');
$this->assertTrue(in_array($new_link_path, $paths), 'Shortcut path changed: ' . $new_link_path); $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.'); $this->assertLinkByHref($new_link_path, 0, 'Shortcut with new path appears on the page.');
} }

View File

@ -73,8 +73,8 @@ abstract class ShortcutTestBase extends WebTestBase {
} }
// Create users. // 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->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')); $this->shortcut_user = $this->drupalCreateUser(array('customize shortcut links', 'switch shortcut sets', 'access shortcuts', 'access content'));
// Create a node. // Create a node.
$this->node = $this->drupalCreateNode(array('type' => 'article')); $this->node = $this->drupalCreateNode(array('type' => 'article'));

View File

@ -249,6 +249,11 @@ function system_permission() {
'title' => t('View site reports'), 'title' => t('View site reports'),
'restrict access' => TRUE, '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,
],
); );
} }

View File

@ -9,7 +9,6 @@ namespace Drupal\user\Access;
use Drupal\Core\Routing\Access\AccessInterface; use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\Request;
/** /**
* Determines access to routes based on login status of current user. * Determines access to routes based on login status of current user.
@ -19,16 +18,14 @@ class LoginStatusCheck implements AccessInterface {
/** /**
* Checks access. * Checks access.
* *
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param \Drupal\Core\Session\AccountInterface $account * @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account. * The currently logged in account.
* *
* @return string * @return string
* A \Drupal\Core\Access\AccessInterface constant value. * A \Drupal\Core\Access\AccessInterface constant value.
*/ */
public function access(Request $request, AccountInterface $account) { public function access(AccountInterface $account) {
return ($request->attributes->get('_menu_admin') || $account->isAuthenticated()) ? static::ALLOW : static::DENY; return $account->isAuthenticated() ? static::ALLOW : static::DENY;
} }
} }

View File

@ -9,7 +9,6 @@ namespace Drupal\user\Access;
use Drupal\Core\Routing\Access\AccessInterface; use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\Request;
/** /**
* Access check for user registration routes. * Access check for user registration routes.
@ -19,15 +18,13 @@ class RegisterAccessCheck implements AccessInterface {
/** /**
* Checks access. * Checks access.
* *
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param \Drupal\Core\Session\AccountInterface $account * @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account. * The currently logged in account.
* *
* @return string * @return string
* A \Drupal\Core\Access\AccessInterface constant value. * A \Drupal\Core\Access\AccessInterface constant value.
*/ */
public function access(Request $request, AccountInterface $account) { public function access(AccountInterface $account) {
return ($request->attributes->get('_menu_admin') || $account->isAnonymous()) && (\Drupal::config('user.settings')->get('register') != USER_REGISTER_ADMINISTRATORS_ONLY) ? static::ALLOW : static::DENY; return ($account->isAnonymous()) && (\Drupal::config('user.settings')->get('register') != USER_REGISTER_ADMINISTRATORS_ONLY) ? static::ALLOW : static::DENY;
} }
} }

View File

@ -272,6 +272,22 @@ class UrlHelperTest extends UnitTestCase {
'fragment' => 'footer', 'fragment' => 'footer',
), ),
), ),
array(
'http://',
array(
'path' => '',
'query' => array(),
'fragment' => '',
),
),
array(
'https://',
array(
'path' => '',
'query' => array(),
'fragment' => '',
),
),
array( array(
'/my/path?destination=home#footer', '/my/path?destination=home#footer',
array( array(

View File

@ -319,6 +319,22 @@ class DrupalTest extends UnitTestCase {
$this->assertNotNull(\Drupal::formBuilder()); $this->assertNotNull(\Drupal::formBuilder());
} }
/**
* 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. * Sets up a mock expectation for the container get() method.
* *

View File

@ -76,15 +76,10 @@ class ExternalUrlTest extends UnitTestCase {
* *
* @covers ::createFromRequest() * @covers ::createFromRequest()
* *
* @expectedException \Drupal\Core\Routing\MatchingRouteNotFoundException * @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
* @expectedExceptionMessage No matching route could be found for the request: request_as_a_string
*/ */
public function testCreateFromRequest() { public function testCreateFromRequest() {
// Mock the request in order to override the __toString() method. $request = Request::create('/test-path');
$request = $this->getMock('Symfony\Component\HttpFoundation\Request');
$request->expects($this->once())
->method('__toString')
->will($this->returnValue('request_as_a_string'));
$this->router->expects($this->once()) $this->router->expects($this->once())
->method('matchRequest') ->method('matchRequest')

View File

@ -44,7 +44,7 @@ class DefaultMenuLinkTreeManipulatorsTest extends UnitTestCase {
/** /**
* The original menu tree build in mockTree(). * The original menu tree build in mockTree().
* *
* @var \Drupal\Tests\Core\Menu\MenuLinkMock[] * @var \Drupal\Core\Menu\MenuLinkTreeElement[]
*/ */
protected $originalTree = array(); protected $originalTree = array();
@ -134,6 +134,7 @@ class DefaultMenuLinkTreeManipulatorsTest extends UnitTestCase {
* Tests the checkAccess() tree manipulator. * Tests the checkAccess() tree manipulator.
* *
* @covers ::checkAccess * @covers ::checkAccess
* @covers ::menuLinkCheckAccess
*/ */
public function testCheckAccess() { public function testCheckAccess() {
// Those menu links that are non-external will have their access checks // Those menu links that are non-external will have their access checks
@ -178,6 +179,31 @@ class DefaultMenuLinkTreeManipulatorsTest extends UnitTestCase {
$this->assertFalse(array_key_exists(8, $tree)); $this->assertFalse(array_key_exists(8, $tree));
} }
/**
* 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. * Tests the flatten() tree manipulator.
* *

View File

@ -0,0 +1,326 @@
<?php
/**
* @file
* Contains \Drupal\Tests\Core\Path\PathValidatorTest.
*/
namespace Drupal\Tests\Core\Path;
use Drupal\Core\ParamConverter\ParamNotConvertedException;
use Drupal\Core\Path\PathValidator;
use Drupal\Tests\UnitTestCase;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
/**
* @coversDefaultClass \Drupal\Core\Path\PathValidator
* @group Routing
*/
class PathValidatorTest extends UnitTestCase {
/**
* The mocked access aware router.
*
* @var \Drupal\Core\Routing\AccessAwareRouterInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $accessAwareRouter;
/**
* The mocked access unaware router.
* @var \Symfony\Component\Routing\Matcher\UrlMatcherInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $accessUnawareRouter;
/**
* The mocked account.
*
* @var \Drupal\Core\Session\AccountInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $account;
/**
* The path processor.
*
* @var \Drupal\Core\PathProcessor\InboundPathProcessorInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $pathProcessor;
/**
* The tested path validator.
*
* @var \Drupal\Core\Path\PathValidator
*/
protected $pathValidator;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->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('<front>'));
}
/**
* 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('<front>?hei=sen#berg');
$this->assertEquals('<front>', $url->getRouteName());
$this->assertEquals(['hei' => 'sen'], $url->getOptions()['query']);
$this->assertEquals('berg', $url->getOptions()['fragment']);
}
}

View File

@ -72,22 +72,27 @@ class UrlTest extends UnitTestCase {
* @covers ::createFromPath() * @covers ::createFromPath()
*/ */
public function testCreateFromPath() { public function testCreateFromPath() {
$this->router->expects($this->any()) $this->router->expects($this->at(0))
->method('match') ->method('matchRequest')
->will($this->returnValueMap(array( ->with(Request::create('/node'))
array('/node', array( ->willReturn([
RouteObjectInterface::ROUTE_NAME => 'view.frontpage.page_1', RouteObjectInterface::ROUTE_NAME => 'view.frontpage.page_1',
'_raw_variables' => new ParameterBag(), '_raw_variables' => new ParameterBag(),
)), ]);
array('/node/1', array( $this->router->expects($this->at(1))
RouteObjectInterface::ROUTE_NAME => 'node_view', ->method('matchRequest')
'_raw_variables' => new ParameterBag(array('node' => '1')), ->with(Request::create('/node/1'))
)), ->willReturn([
array('/node/2/edit', array( RouteObjectInterface::ROUTE_NAME => 'node_view',
RouteObjectInterface::ROUTE_NAME => 'node_edit', '_raw_variables' => new ParameterBag(['node' => '1']),
'_raw_variables' => new ParameterBag(array('node' => '2')), ]);
)), $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(); $urls = array();
foreach ($this->map as $index => $values) { foreach ($this->map as $index => $values) {
@ -114,13 +119,12 @@ class UrlTest extends UnitTestCase {
* *
* @covers ::createFromPath() * @covers ::createFromPath()
* *
* @expectedException \Drupal\Core\Routing\MatchingRouteNotFoundException * @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
* @expectedExceptionMessage No matching route could be found for the path "non-existent"
*/ */
public function testCreateFromPathInvalid() { public function testCreateFromPathInvalid() {
$this->router->expects($this->once()) $this->router->expects($this->once())
->method('match') ->method('matchRequest')
->with('/non-existent') ->with(Request::create('/non-existent'))
->will($this->throwException(new ResourceNotFoundException())); ->will($this->throwException(new ResourceNotFoundException()));
$this->assertNull(Url::createFromPath('non-existent')); $this->assertNull(Url::createFromPath('non-existent'));
@ -155,15 +159,10 @@ class UrlTest extends UnitTestCase {
* *
* @covers ::createFromRequest() * @covers ::createFromRequest()
* *
* @expectedException \Drupal\Core\Routing\MatchingRouteNotFoundException * @expectedException \Symfony\Component\Routing\Exception\ResourceNotFoundException
* @expectedExceptionMessage No matching route could be found for the request: request_as_a_string
*/ */
public function testCreateFromRequestInvalid() { public function testCreateFromRequestInvalid() {
// Mock the request in order to override the __toString() method. $request = Request::create('/test-path');
$request = $this->getMock('Symfony\Component\HttpFoundation\Request');
$request->expects($this->once())
->method('__toString')
->will($this->returnValue('request_as_a_string'));
$this->router->expects($this->once()) $this->router->expects($this->once())
->method('matchRequest') ->method('matchRequest')