Commit 68f576fe authored by webchick's avatar webchick

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

parent aca1ec38
......@@ -506,7 +506,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
......
......@@ -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');
}
}
......@@ -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]);
......
......@@ -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();
......
......@@ -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 == '<front>' || 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'];
}
if ($parsed_url['path'] == '<front>') {
return new Url('<front>', [], $options);
}
elseif (UrlHelper::isExternal($path) && UrlHelper::isValid($path)) {
if (empty($parsed_url['path'])) {
return FALSE;
}
return Url::createFromPath($path);
}
// Check the routing system.
$collection = $this->routeProvider->getRoutesByPattern('/' . $path);
if ($collection->count() == 0) {
$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;
}
}
......@@ -2,7 +2,7 @@
/**
* @file
* Contains Drupal\Core\Path\PathValidatorInterface
* Contains \Drupal\Core\Path\PathValidatorInterface
*/
namespace Drupal\Core\Path;
......@@ -12,6 +12,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.
*
......
......@@ -22,6 +22,9 @@ interface InboundPathProcessorInterface {
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The HttpRequest object representing the current request.
*
* @return string
* The processed path.
*/
public function processInbound($path, Request $request);
......
......@@ -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 == '<front>') {
$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);
......
......@@ -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'];
}
catch (NotFoundHttpException $e) {
// Nothing to do here, LinkTypeConstraintValidator emits errors.
}
catch (MatchingRouteNotFoundException $e) {
// Nothing to do here, LinkTypeConstraintValidator emits errors.
$url = \Drupal::pathValidator()->getUrlIfValid($value['url']);
if (!$url) {
return $values;
}
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() . '/'));
}
}
}
......
......@@ -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,36 +48,20 @@ 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)) {
$url_is_valid = FALSE;
}
elseif ($url->isExternal() && !($link_type & LinkItemInterface::LINK_EXTERNAL)) {
if ($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) {
$this->context->addViolation($this->message, array('%url' => $url_string));
......
......@@ -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);
}
......
......@@ -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;
......@@ -77,6 +75,13 @@ class MenuLinkContentForm extends ContentEntityForm implements MenuLinkFormInter
*/
protected $account;
/**
* The path validator.
*
* @var \Drupal\Core\Path\PathValidatorInterface
*/
protected $pathValidator;
/**
* Constructs a MenuLinkContentForm object.
*
......@@ -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')
);
}
......@@ -167,46 +176,6 @@ public function submitConfigurationForm(array &$form, FormStateInterface $form_s
$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}
*/
......@@ -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,
)));
}
}
}
}
......@@ -258,29 +258,6 @@ function shortcut_set_title_exists($title) {
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.
*
......
......@@ -8,6 +8,7 @@
namespace Drupal\shortcut\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Path\PathValidatorInterface;
use Drupal\shortcut\ShortcutSetInterface;