Commit 65c808b1 authored by alexpott's avatar alexpott

Issue #2153891 by tim.plunkett, amateescu: Add a Url value object.

parent f17c8b31
......@@ -9,6 +9,8 @@
/**
* Helper class URL based methods.
*
* @todo Rename to UrlHelper in https://drupal.org/node/2184653.
*/
class Url {
......
......@@ -8,7 +8,8 @@
namespace Drupal\Core\Form;
use Drupal\Component\Utility\String;
use Drupal\Component\Utility\Url;
use Drupal\Component\Utility\Url as UrlHelper;
use Drupal\Core\Url;
use Symfony\Component\HttpFoundation\Request;
/**
......@@ -36,7 +37,7 @@ public static function buildCancelLink(ConfirmFormInterface $form, Request $requ
$query = $request->query;
// If a destination is specified, that serves as the cancel link.
if ($query->has('destination')) {
$options = Url::parse($query->get('destination'));
$options = UrlHelper::parse($query->get('destination'));
$link = array(
'#href' => $options['path'],
'#options' => $options,
......@@ -44,19 +45,19 @@ public static function buildCancelLink(ConfirmFormInterface $form, Request $requ
}
// Check for a route-based cancel link.
elseif ($route = $form->getCancelRoute()) {
if (empty($route['route_name'])) {
throw new \UnexpectedValueException(String::format('Missing route name in !class::getCancelRoute().', array('!class' => get_class($form))));
if (!($route instanceof Url)) {
if (empty($route['route_name'])) {
throw new \UnexpectedValueException(String::format('Missing route name in !class::getCancelRoute().', array('!class' => get_class($form))));
}
// Ensure there is something to pass as the params and options.
$route += array(
'route_parameters' => array(),
'options' => array(),
);
$route = new Url($route['route_name'], $route['route_parameters'], $route['options']);
}
// Ensure there is something to pass as the params and options.
$route += array(
'route_parameters' => array(),
'options' => array(),
);
$link = array(
'#route_name' => $route['route_name'],
'#route_parameters' => $route['route_parameters'],
'#options' => $route['options'],
);
$link = $route->toRenderArray();
}
$link['#type'] = 'link';
......
......@@ -10,7 +10,7 @@
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\Unicode;
use Drupal\Component\Utility\Url;
use Drupal\Component\Utility\Url as UrlHelper;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\HttpKernel;
......@@ -18,6 +18,7 @@
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Url;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
......@@ -847,7 +848,7 @@ public function validateForm($form_id, &$form, &$form_state) {
if (isset($form['#token'])) {
if (!$this->csrfToken->validate($form_state['values']['form_token'], $form['#token'])) {
$path = $this->request->attributes->get('_system_path');
$query = Url::filterQueryParameters($this->request->query->all());
$query = UrlHelper::filterQueryParameters($this->request->query->all());
$url = $this->urlGenerator->generateFromPath($path, array('query' => $query));
// Setting this error will cause the form to fail validation.
......@@ -933,13 +934,17 @@ public function redirectForm($form_state) {
// Check for a route-based redirection.
if (isset($form_state['redirect_route'])) {
$form_state['redirect_route'] += array(
'route_parameters' => array(),
'options' => array(),
);
$form_state['redirect_route']['options']['absolute'] = TRUE;
$url = $this->urlGenerator->generateFromRoute($form_state['redirect_route']['route_name'], $form_state['redirect_route']['route_parameters'], $form_state['redirect_route']['options']);
return new RedirectResponse($url);
// @todo Remove once all redirects are converted to Url.
if (!($form_state['redirect_route'] instanceof Url)) {
$form_state['redirect_route'] += array(
'route_parameters' => array(),
'options' => array(),
);
$form_state['redirect_route'] = new Url($form_state['redirect_route']['route_name'], $form_state['redirect_route']['route_parameters'], $form_state['redirect_route']['options']);
}
$form_state['redirect_route']->setAbsolute();
return new RedirectResponse($form_state['redirect_route']->toString());
}
// Only invoke a redirection if redirect value was not set to FALSE.
......@@ -1318,7 +1323,7 @@ public function doBuildForm($form_id, &$element, &$form_state) {
// Special handling if we're on the top level form element.
if (isset($element['#type']) && $element['#type'] == 'form') {
if (!empty($element['#https']) && settings()->get('mixed_mode_sessions', FALSE) &&
!Url::isExternal($element['#action'])) {
!UrlHelper::isExternal($element['#action'])) {
global $base_root;
// Not an external URL so ensure that it is secure.
......
<?php
/**
* @file
* Contains \Drupal\Core\Routing\MatchingRouteNotFoundException.
*/
namespace Drupal\Core\Routing;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
/**
* No matching route was found.
*/
class MatchingRouteNotFoundException extends ResourceNotFoundException {
}
......@@ -42,33 +42,4 @@ public function finalMatch(RouteCollection $collection, Request $request) {
return $this->match($path);
}
/**
* Returns the route_name and route parameters matching a system path.
*
* @todo Find a better place for this method in
* https://drupal.org/node/2153891.
*
* @param string $link_path
* The link path to find a route name for.
*
* @return array
* Returns an array with both the route name and parameters, or an empty
* array if no route was matched.
*/
public function findRouteNameParameters($link_path) {
// Look up the route_name used for the given path.
$request = Request::create('/' . $link_path);
$request->attributes->set('_system_path', $link_path);
try {
$result = \Drupal::service('router')->matchRequest($request);
$return = array();
$return[] = isset($result['_route']) ? $result['_route'] : '';
$return[] = $result['_raw_variables']->all();
return $return;
}
catch (\Exception $e) {
return array();
}
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Url.
*/
namespace Drupal\Core;
use Drupal\Component\Utility\Url as UrlHelper;
use Drupal\Core\DependencyInjection\DependencySerialization;
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.
*/
class Url extends DependencySerialization {
/**
* The URL generator.
*
* @var \Drupal\Core\Routing\UrlGeneratorInterface
*/
protected $urlGenerator;
/**
* The route name.
*
* @var string
*/
protected $routeName;
/**
* The route parameters.
*
* @var array
*/
protected $routeParameters = array();
/**
* The URL options.
*
* @var array
*/
protected $options = array();
/**
* Indicates whether this URL is external.
*
* @var bool
*/
protected $external = FALSE;
/**
* The external path.
*
* Only used if self::$external is TRUE.
*
* @var string
*/
protected $path;
/**
* Constructs a new Url object.
*
* @param string $route_name
* The name of the route
* @param array $route_parameters
* (optional) An associative array of parameter names and values.
* @param array $options
* (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.
* - '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
* absolute link (beginning with http:). Useful for links that will be
* displayed outside the site, such as in an RSS feed.
* - 'language': An optional language object used to look up the alias
* for the URL. If $options['language'] is omitted, the language will be
* obtained from language(Language::TYPE_URL).
* - 'https': Whether this URL should point to a secure location. If not
* defined, the current scheme is used, so the user stays on HTTP or HTTPS
* respectively. if mixed mode sessions are permitted, TRUE enforces HTTPS
* and FALSE enforces HTTP.
*/
public function __construct($route_name, $route_parameters = array(), $options = array()) {
$this->routeName = $route_name;
$this->routeParameters = $route_parameters;
$this->options = $options;
}
/**
* Returns the Url object matching a path.
*
* @param string $path
* A path (e.g. 'node/1', 'http://drupal.org').
*
* @return static
* An Url object.
*
* @throws \Drupal\Core\Routing\MatchingRouteNotFoundException
* Thrown when the path cannot be matched.
*/
public static function createFromPath($path) {
if (UrlHelper::isExternal($path)) {
$url = new static($path);
$url->setExternal();
return $url;
}
// Special case the front page route.
if ($path == '<front>') {
$route_name = $path;
$route_parameters = array();
}
else {
// Look up the route name and parameters used for the given path.
try {
$result = \Drupal::service('router')->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.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* A request object.
*
* @return static
* A Url object.
*
* @throws \Drupal\Core\Routing\MatchingRouteNotFoundException
* Thrown when the request cannot be matched.
*/
public static function createFromRequest(Request $request) {
try {
$result = \Drupal::service('router')->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_parameters = $result['_raw_variables']->all();
return new static($route_name, $route_parameters);
}
/**
* Sets this Url to be external.
*
* @return $this
*/
protected function setExternal() {
$this->external = TRUE;
// What was passed in as the route name is actually the path.
$this->path = $this->routeName;
// Set empty route name and parameters.
$this->routeName = '';
$this->routeParameters = array();
return $this;
}
/**
* Indicates if this Url is external.
*
* @return bool
*/
public function isExternal() {
return $this->external;
}
/**
* Returns the route name.
*
* @return string
*/
public function getRouteName() {
return $this->routeName;
}
/**
* Returns the route parameters.
*
* @return array
*/
public function getRouteParameters() {
return $this->routeParameters;
}
/**
* Sets the route parameters.
*
* @param array $parameters
* The array of parameters.
*
* @return $this
*/
public function setRouteParameters($parameters) {
if ($this->isExternal()) {
throw new \Exception('External URLs do not have route parameters.');
}
$this->routeParameters = $parameters;
return $this;
}
/**
* Sets a specific route parameter.
*
* @param string $key
* The key of the route parameter.
* @param mixed $value
* The route parameter.
*
* @return $this
*/
public function setRouteParameter($key, $value) {
if ($this->isExternal()) {
throw new \Exception('External URLs do not have route parameters.');
}
$this->routeParameters[$key] = $value;
return $this;
}
/**
* Returns the URL options.
*
* @return array
*/
public function getOptions() {
return $this->options;
}
/**
* Gets a specific option.
*
* @param string $name
* The name of the option.
*
* @return mixed
* The value for a specific option, or NULL if it does not exist.
*/
public function getOption($name) {
if (!isset($this->options[$name])) {
return NULL;
}
return $this->options[$name];
}
/**
* Sets the URL options.
*
* @param array $options
* The array of options.
*
* @return $this
*/
public function setOptions($options) {
$this->options = $options;
return $this;
}
/**
* Sets a specific option.
*
* @param string $name
* The name of the option.
* @param mixed $value
* The option value.
*
* @return $this
*/
public function setOption($name, $value) {
$this->options[$name] = $value;
return $this;
}
/**
* Sets the absolute value for this Url.
*
* @param bool $absolute
* (optional) Whether to make this Url absolute or not. Defaults to TRUE.
*
* @return $this
*/
public function setAbsolute($absolute = TRUE) {
$this->options['absolute'] = $absolute;
return $this;
}
/**
* Generates the path for this Url object.
*/
public function toString() {
if ($this->isExternal()) {
return $this->urlGenerator()->generateFromPath($this->path, $this->getOptions());
}
return $this->urlGenerator()->generateFromRoute($this->getRouteName(), $this->getRouteParameters(), $this->getOptions());
}
/**
* Returns all the information about the route.
*
* @return array
* An associative array containing all the properties of the route.
*/
public function toArray() {
return array(
'route_name' => $this->getRouteName(),
'route_parameters' => $this->getRouteParameters(),
'options' => $this->getOptions(),
);
}
/**
* Returns the route information for a render array.
*
* @return array
* An associative array suitable for a render array.
*/
public function toRenderArray() {
return array(
'#route_name' => $this->getRouteName(),
'#route_parameters' => $this->getRouteParameters(),
'#options' => $this->getOptions(),
);
}
/**
* Returns the internal path for this route.
*
* This path will not include any prefixes, fragments, or query strings.
*
* @return string
* The internal path for this route.
*/
public function getInternalPath() {
if ($this->isExternal()) {
throw new \Exception('External URLs do not have internal representations.');
}
return $this->urlGenerator()->getPathFromRoute($this->getRouteName(), $this->getRouteParameters());
}
/**
* Gets the URL generator.
*
* @return \Drupal\Core\Routing\UrlGeneratorInterface
* The URL generator.
*/
protected function urlGenerator() {
if (!$this->urlGenerator) {
$this->urlGenerator = \Drupal::urlGenerator();
}
return $this->urlGenerator;
}
/**
* Sets the URL generator.
*
* @param \Drupal\Core\Routing\UrlGeneratorInterface
* The URL generator.
*
* @return $this
*/
public function setUrlGenerator(UrlGeneratorInterface $url_generator) {
$this->urlGenerator = $url_generator;
return $this;
}
}
......@@ -13,6 +13,7 @@
use Drupal\Core\Path\AliasManagerInterface;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\Url;
/**
* Provides a class which generates a link with route names and parameters.
......@@ -68,14 +69,13 @@ public function __construct(UrlGeneratorInterface $url_generator, ModuleHandlerI
*
* @see system_page_build()
*/
public function generate($text, $route_name, array $parameters = array(), array $options = array()) {
public function generateFromUrl($text, Url $url) {
// Start building a structured representation of our link to be altered later.
$variables = array(
// @todo Inject the service when drupal_render() is converted to one.
'text' => is_array($text) ? drupal_render($text) : $text,
'route_name' => $route_name,
'parameters' => $parameters,
'options' => $options,
'url' => $url,
'options' => $url->getOptions(),
);
// Merge in default options.
......@@ -85,6 +85,7 @@ public function generate($text, $route_name, array $parameters = array(), array
'html' => FALSE,
'language' => NULL,
'set_active_class' => FALSE,
'absolute' => FALSE,
);
// Add a hreflang attribute if we know the language of this link's url and
......@@ -106,7 +107,7 @@ public function generate($text, $route_name, array $parameters = array(), array
// Add a "data-drupal-link-system-path" attribute to let the
// drupal.active-link library know the path in a standardized manner.
if (!isset($variables['options']['attributes']['data-drupal-link-system-path'])) {
$path = $this->urlGenerator->getPathFromRoute($route_name, $parameters);
$path = $url->getInternalPath();
$variables['options']['attributes']['data-drupal-link-system-path'] = $this->aliasManager->getSystemPath($path);
}
}
......@@ -123,10 +124,11 @@ public function generate($text, $route_name, array $parameters = array(), array
// Move attributes out of options. generateFromRoute(() doesn't need them.
$attributes = new Attribute($variables['options']['attributes']);
unset($variables['options']['attributes']);
$url->setOptions($variables['options']);
// The result of the url generator is a plain-text URL. Because we are using
// it here in an HTML argument context, we need to encode it properly.
$url = String::checkPlain($this->urlGenerator->generateFromRoute($variables['route_name'], $variables['parameters'], $variables['options']));
$url = String::checkPlain($url->toString());
// Sanitize the link text if necessary.
$text = $variables['options']['html'] ? $variables['text'] : String::checkPlain($variables['text']);
......@@ -134,4 +136,13 @@ public function generate($text, $route_name, array $parameters = array(), array
return '<a href="' . $url . '"' . $attributes . '>' . $text . '</a>';
}
/**
* {@inheritdoc}
*/
public function generate($text, $route_name, array $parameters = array(), array $options = array()) {
$url = new Url($route_name, $parameters, $options);
$url->setUrlGenerator($this->urlGenerator);
return $this->generateFromUrl($text, $url);
}
}
......@@ -7,6 +7,8 @@
namespace Drupal\Core\Utility;
use Drupal\Core\Url;
/**
* Defines an interface for generating links from route names and parameters.
*/
......@@ -77,4 +79,17 @@ interface LinkGeneratorInterface {
*/
public function generate($text, $route_name, array $parameters = array(), array $options = array());