diff --git a/core/core.services.yml b/core/core.services.yml index 14c665c..483d030 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -685,7 +685,7 @@ services: - { name: event_subscriber } url_generator: class: Drupal\Core\Routing\UrlGenerator - arguments: ['@router.route_provider', '@path_processor_manager', '@route_processor_manager', '@config.factory', '@logger.channel.default', '@request_stack'] + arguments: ['@router.route_provider', '@path_processor_manager', '@route_processor_manager', '@config.factory', '@request_stack'] calls: - [setContext, ['@?router.request_context']] redirect.destination: diff --git a/core/lib/Drupal/Core/Routing/NullGenerator.php b/core/lib/Drupal/Core/Routing/NullGenerator.php index f78ce63..7fedcb5 100644 --- a/core/lib/Drupal/Core/Routing/NullGenerator.php +++ b/core/lib/Drupal/Core/Routing/NullGenerator.php @@ -56,7 +56,7 @@ protected function processRoute($name, Route $route, array &$parameters) { /** * {@inheritdoc} */ - protected function getInternalPathFromRoute(Route $route, $parameters = array()) { + protected function getInternalPathFromRoute(Route $route, $parameters = array(), $query_params = array()) { return $route->getPath(); } diff --git a/core/lib/Drupal/Core/Routing/UrlGenerator.php b/core/lib/Drupal/Core/Routing/UrlGenerator.php index ba84275..925c155 100644 --- a/core/lib/Drupal/Core/Routing/UrlGenerator.php +++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php @@ -8,23 +8,27 @@ namespace Drupal\Core\Routing; use Psr\Log\LoggerInterface; -use Symfony\Component\HttpFoundation\Request; +use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\HttpFoundation\RequestStack; - +use Symfony\Component\Routing\RequestContext as SymfonyRequestContext; use Symfony\Component\Routing\Route as SymfonyRoute; use Symfony\Component\Routing\Exception\RouteNotFoundException; - -use Symfony\Cmf\Component\Routing\ProviderBasedGenerator; - use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\PathProcessor\OutboundPathProcessorInterface; use Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface; +use Symfony\Component\Routing\Exception\InvalidParameterException; +use Symfony\Component\Routing\Exception\MissingMandatoryParametersException; /** * Generates URLs from route names and parameters. */ -class UrlGenerator extends ProviderBasedGenerator implements UrlGeneratorInterface { +class UrlGenerator implements UrlGeneratorInterface { + + /** + * @var RequestContext + */ + protected $context; /** * A request stack object. @@ -70,13 +74,12 @@ class UrlGenerator extends ProviderBasedGenerator implements UrlGeneratorInterfa * The route processor. * @param \Drupal\Core\Config\ConfigFactoryInterface $config * The config factory. - * @param \Psr\Log\LoggerInterface $logger - * An optional logger for recording errors. * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack * A request stack object. */ - public function __construct(RouteProviderInterface $provider, OutboundPathProcessorInterface $path_processor, OutboundRouteProcessorInterface $route_processor, ConfigFactoryInterface $config, LoggerInterface $logger = NULL, RequestStack $request_stack) { - parent::__construct($provider, $logger); + public function __construct(RouteProviderInterface $provider, OutboundPathProcessorInterface $path_processor, OutboundRouteProcessorInterface $route_processor, ConfigFactoryInterface $config, RequestStack $request_stack) { + $this->provider = $provider; + $this->context = new RequestContext(); $this->pathProcessor = $path_processor; $this->routeProcessor = $route_processor; @@ -88,6 +91,34 @@ public function __construct(RouteProviderInterface $provider, OutboundPathProces /** * {@inheritdoc} */ + public function setContext(SymfonyRequestContext $context) { + $this->context = $context; + } + + /** + * {@inheritdoc} + */ + public function getContext() { + return $this->context; + } + + /** + * {@inheritdoc} + */ + public function setStrictRequirements($enabled) { + // Ignore changes to this. + } + + /** + * {@inheritdoc} + */ + public function isStrictRequirements() { + return TRUE; + } + + /** + * {@inheritdoc} + */ public function getPathFromRoute($name, $parameters = array()) { $route = $this->getRoute($name); $this->processRoute($name, $route, $parameters); @@ -100,6 +131,100 @@ public function getPathFromRoute($name, $parameters = array()) { } /** + * Substitute the route parameters into the route path. + * + * Note: This code was copied from + * \Symfony\Component\Routing\Generator\UrlGenerator::doGenerate() and modified + * to remove logic that is not relevant to Drupal or that is handled + * separately in ::generateFromRoute(). The Symfony version should be examined + * for useful fixes or improvements over time. + * + * @return string + * The url path, without any base path, including possible query string. + * + * @throws MissingMandatoryParametersException + * When some parameters are missing that are mandatory for the route + * @throws InvalidParameterException + * When a parameter value for a placeholder is not correct because it does + * not match the requirement + */ + protected function doGenerate(array $variables, array $defaults, array $tokens, array $parameters, array $query_params, $name) { + $variables = array_flip($variables); + $mergedParams = array_replace($defaults, $this->context->getParameters(), $parameters); + + // all params must be given + if ($diff = array_diff_key($variables, $mergedParams)) { + throw new MissingMandatoryParametersException(sprintf('Some mandatory parameters are missing ("%s") to generate a URL for route "%s".', implode('", "', array_keys($diff)), $name)); + } + + $url = ''; + // Tokens start from the end of the path and work to the beginning. The the + // first one or several variable tokens may be optional, but once we find a + // supplied token or a static text portion of the path, all remaining + // variables up to the start of the path must be supplied to there is no gap. + $optional = TRUE; + // Structure of $tokens from the compiled route: + // If the path is /admin/config/user-interface/shortcut/manage/{shortcut_set}/add-link-inline + // [ [ 0 => 'text', 1 => '/add-link-inline' ], [ 0 => 'variable', 1 => '/', 2 => '[^/]++', 3 => 'shortcut_set' ], [ 0 => 'text', 1 => '/admin/config/user-interface/shortcut/manage' ] ] + // + // For a simple fixed path, there is just one token. + // If the path is /admin/config + // [ [ 0 => 'text', 1 => '/admin/config' ] ] + foreach ($tokens as $token) { + if ('variable' === $token[0]) { + if (!$optional || !array_key_exists($token[3], $defaults) || (isset($mergedParams[$token[3]]) && (string) $mergedParams[$token[3]] !== (string) $defaults[$token[3]])) { + // check requirement + if (!preg_match('#^'.$token[2].'$#', $mergedParams[$token[3]])) { + $message = sprintf('Parameter "%s" for route "%s" must match "%s" ("%s" given) to generate a corresponding URL.', $token[3], $name, $token[2], $mergedParams[$token[3]]); + throw new InvalidParameterException($message); + } + + $url = $token[1] . $mergedParams[$token[3]] . $url; + $optional = FALSE; + } + } + else { + // Static text + $url = $token[1] . $url; + $optional = FALSE; + } + } + + if ('' === $url) { + $url = '/'; + } + + // the contexts base URL is already encoded (see Symfony\Component\HttpFoundation\Request) + $url = strtr(rawurlencode($url), $this->decodedChars); + + // Drupal paths rarely include dots, so skip this processing if possible. + if (strpos($url, '/.') !== FALSE) { + // the path segments "." and ".." are interpreted as relative reference when + // resolving a URI; see http://tools.ietf.org/html/rfc3986#section-3.3 + // so we need to encode them as they are not used for this purpose here + // otherwise we would generate a URI that, when followed by a user agent + // (e.g. browser), does not match this route + $url = strtr($url, array('/../' => '/%2E%2E/', '/./' => '/%2E/')); + if ('/..' === substr($url, -3)) { + $url = substr($url, 0, -2) . '%2E%2E'; + } + elseif ('/.' === substr($url, -2)) { + $url = substr($url, 0, -1) . '%2E'; + } + } + + // Add a query string if needed, including extra parameters. + $query_params += array_diff_key($parameters, $variables, $defaults); + if ($query_params && $query = http_build_query($query_params, '', '&')) { + // "/" and "?" can be left decoded for better user experience, see + // http://tools.ietf.org/html/rfc3986#section-3.4 + $url .= '?'.strtr($query, array('%2F' => '/')); + } + + return $url; + } + + /** * Gets the path of a route. * * @param \Symfony\Component\Routing\Route $route @@ -107,32 +232,19 @@ public function getPathFromRoute($name, $parameters = array()) { * @param array $parameters * An array of parameters as passed to * \Symfony\Component\Routing\Generator\UrlGeneratorInterface::generate(). + * @param array $query_params + * An array of query string parameter, which will get any extra values from + * $parameters merged in. * * @return string * The url path corresponding to the route, without the base path. */ - protected function getInternalPathFromRoute(SymfonyRoute $route, $parameters = array()) { + protected function getInternalPathFromRoute(SymfonyRoute $route, $parameters = array(), $query_params = array()) { // The Route has a cache of its own and is not recompiled as long as it does // not get modified. $compiledRoute = $route->compile(); - $hostTokens = $compiledRoute->getHostTokens(); - - $route_requirements = $route->getRequirements(); - // We need to bypass the doGenerate() method's handling of absolute URLs as - // we handle that ourselves after processing the path. - if (isset($route_requirements['_scheme'])) { - unset($route_requirements['_scheme']); - } - $path = $this->doGenerate($compiledRoute->getVariables(), $route->getDefaults(), $route_requirements, $compiledRoute->getTokens(), $parameters, $route->getPath(), FALSE, $hostTokens); - // The URL returned from doGenerate() will include the base path if there is - // one (i.e., if running in a subdirectory) so we need to strip that off - // before processing the path. - $base_url = $this->context->getBaseUrl(); - if (!empty($base_url) && strpos($path, $base_url) === 0) { - $path = substr($path, strlen($base_url)); - } - return $path; + return $this->doGenerate($compiledRoute->getVariables(), $route->getDefaults(), $compiledRoute->getTokens(), $parameters, $query_params, $route->getPath()); } /** @@ -151,12 +263,13 @@ public function generateFromRoute($name, $parameters = array(), $options = array $route = $this->getRoute($name); $this->processRoute($name, $route, $parameters); + $query_params = []; // Symfony adds any parameters that are not path slugs as query strings. if (isset($options['query']) && is_array($options['query'])) { - $parameters = (array) $parameters + $options['query']; + $query_params = $options['query']; } - $path = $this->getInternalPathFromRoute($route, $parameters); + $path = $this->getInternalPathFromRoute($route, $parameters, $query_params); $path = $this->processPath($path, $options); if (!empty($options['prefix'])) { diff --git a/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php b/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php index 9b9bc4f..e5c58a6 100644 --- a/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php +++ b/core/lib/Drupal/Core/Routing/UrlGeneratorInterface.php @@ -7,14 +7,14 @@ namespace Drupal\Core\Routing; -use Symfony\Cmf\Component\Routing\VersatileGeneratorInterface; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface as SymfonyUrlGeneratorInterface; /** * Defines an interface for generating a url from a route or system path. * * Provides additional methods and options not present in the base interface. */ -interface UrlGeneratorInterface extends VersatileGeneratorInterface { +interface UrlGeneratorInterface extends SymfonyUrlGeneratorInterface { /** * Generates an internal or external URL. @@ -120,7 +120,7 @@ public function getPathFromRoute($name, $parameters = array()); * (optional) An associative array of additional options, with the following * elements: * - 'query': An array of query key/value-pairs (without any URL-encoding) - * to append to the URL. Merged with the parameters array. + * to append to the URL. * - 'fragment': A fragment identifier (named anchor) to append to the URL. * Do not include the leading '#' character. * - 'absolute': Defaults to FALSE. Whether to force the output to be an @@ -141,7 +141,7 @@ public function getPathFromRoute($name, $parameters = array()); * The generated URL for the given route. * * @throws \Symfony\Component\Routing\Exception\RouteNotFoundException - * Thrown when the named route doesn't exist. + * Thrown when the named route does not exist. * @throws \Symfony\Component\Routing\Exception\MissingMandatoryParametersException * Thrown when some parameters are missing that are mandatory for the route. * @throws \Symfony\Component\Routing\Exception\InvalidParameterException diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php index ba5d75c..9fe5624 100644 --- a/core/modules/system/src/Controller/DbUpdateController.php +++ b/core/modules/system/src/Controller/DbUpdateController.php @@ -601,7 +601,7 @@ protected function triggerBatch(Request $request) { ); batch_set($batch); - return batch_process('update.php/results', Url::fromRoute('system.db_update')); + return batch_process('update.php/results', Url::fromRoute('system.db_update', array('op' => 'start'))); } /** diff --git a/core/modules/system/src/Tests/Update/UpdateScriptTest.php b/core/modules/system/src/Tests/Update/UpdateScriptTest.php index 83db3fb..83cc388 100644 --- a/core/modules/system/src/Tests/Update/UpdateScriptTest.php +++ b/core/modules/system/src/Tests/Update/UpdateScriptTest.php @@ -7,6 +7,7 @@ namespace Drupal\system\Tests\Update; +use Drupal\Core\Url; use Drupal\simpletest\WebTestBase; /** @@ -44,7 +45,7 @@ class UpdateScriptTest extends WebTestBase { protected function setUp() { parent::setUp(); - $this->updateUrl = $GLOBALS['base_url'] . '/update.php'; + $this->updateUrl = Url::fromRoute('system.db_update'); $this->updateUser = $this->drupalCreateUser(array('administer software updates', 'access site in maintenance mode')); \Drupal::service('entity.definition_update_manager')->applyUpdates(); } diff --git a/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php b/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php index 7e7fdf3..52ab79f 100644 --- a/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php +++ b/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php @@ -129,7 +129,7 @@ protected function setUp() { $config_factory_stub = $this->getConfigFactoryStub(array('system.filter' => array('protocols' => array('http', 'https')))); - $generator = new UrlGenerator($provider, $processor_manager, $this->routeProcessorManager, $config_factory_stub, NULL, $this->requestStack); + $generator = new UrlGenerator($provider, $processor_manager, $this->routeProcessorManager, $config_factory_stub, $this->requestStack); $generator->setContext($context); $this->generator = $generator; }