diff --git a/core/includes/common.inc b/core/includes/common.inc index 5e9ff1c..d4a11fb 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -1964,155 +1964,13 @@ function _format_date_callback(array $matches = NULL, $new_langcode = NULL) { * When creating links in modules, consider whether l() could be a better * alternative than url(). * - * @param $path - * (optional) The internal path or external URL being linked to, such as - * "node/34" or "http://example.com/foo". The default value is equivalent to - * passing in ''. A few notes: - * - If you provide a full URL, it will be considered an external URL. - * - If you provide only the path (e.g. "node/34"), it will be - * considered an internal link. In this case, it should be a system URL, - * and it will be replaced with the alias, if one exists. Additional query - * arguments for internal paths must be supplied in $options['query'], not - * included in $path. - * - If you provide an internal path and $options['alias'] is set to TRUE, the - * path is assumed already to be the correct path alias, and the alias is - * not looked up. - * - The special string '' generates a link to the site's base URL. - * - If your external URL contains a query (e.g. http://example.com/foo?a=b), - * then you can either URL encode the query keys and values yourself and - * include them in $path, or use $options['query'] to let this function - * URL encode them. - * @param $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. - * - '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. - * - 'alias': Defaults to FALSE. Whether the given path is a URL alias - * already. - * - 'external': Whether the given path is an external URL. - * - 'language': An optional language object. If the path being linked to is - * internal to the site, $options['language'] is 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. TRUE enforces HTTPS and FALSE enforces HTTP, but HTTPS can - * only be enforced when the variable 'https' is set to TRUE. - * - 'base_url': Only used internally, to modify the base URL when a language - * dependent URL requires so. - * - 'prefix': Only used internally, to modify the path when a language - * dependent URL requires so. - * - 'script': Added to the URL between the base path and the path prefix. - * Defaults to empty string when clean URLs are in effect, and to - * 'index.php/' when they are not. - * - 'entity_type': The entity type of the object that called url(). Only - * set if url() is invoked by Drupal\Core\Entity\Entity::uri(). - * - 'entity': The entity object (such as a node) for which the URL is being - * generated. Only set if url() is invoked by Drupal\Core\Entity\Entity::uri(). - * - * @return - * A string containing a URL to the given path. + * @see \Drupal\Core\Routing\PathBasedGeneratorInterface::generateFromPath(). */ function url($path = NULL, array $options = array()) { - // Merge in defaults. - $options += array( - 'fragment' => '', - 'query' => array(), - 'absolute' => FALSE, - 'alias' => FALSE, - 'prefix' => '', - 'script' => $GLOBALS['script_path'], - ); - - if (!isset($options['external'])) { - // Return an external link if $path contains an allowed absolute URL. Only - // call the slow drupal_strip_dangerous_protocols() if $path contains a ':' - // before any / ? or #. Note: we could use url_is_external($path) here, but - // that would require another function call, and performance inside url() is - // critical. - $colonpos = strpos($path, ':'); - $options['external'] = ($colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && drupal_strip_dangerous_protocols($path) == $path); - } - - // Preserve the original path before altering or aliasing. - $original_path = $path; - - // Allow other modules to alter the outbound URL and options. - drupal_alter('url_outbound', $path, $options, $original_path); - - if (isset($options['fragment']) && $options['fragment'] !== '') { - $options['fragment'] = '#' . $options['fragment']; - } - - if ($options['external']) { - // Split off the fragment. - if (strpos($path, '#') !== FALSE) { - list($path, $old_fragment) = explode('#', $path, 2); - // If $options contains no fragment, take it over from the path. - if (isset($old_fragment) && !$options['fragment']) { - $options['fragment'] = '#' . $old_fragment; - } - } - // Append the query. - if ($options['query']) { - $path .= (strpos($path, '?') !== FALSE ? '&' : '?') . drupal_http_build_query($options['query']); - } - if (isset($options['https']) && variable_get('https', FALSE)) { - if ($options['https'] === TRUE) { - $path = str_replace('http://', 'https://', $path); - } - elseif ($options['https'] === FALSE) { - $path = str_replace('https://', 'http://', $path); - } - } - // Reassemble. - return $path . $options['fragment']; - } - - global $base_url, $base_secure_url, $base_insecure_url; - - // The base_url might be rewritten from the language rewrite in domain mode. - if (!isset($options['base_url'])) { - if (isset($options['https']) && variable_get('https', FALSE)) { - if ($options['https'] === TRUE) { - $options['base_url'] = $base_secure_url; - $options['absolute'] = TRUE; - } - elseif ($options['https'] === FALSE) { - $options['base_url'] = $base_insecure_url; - $options['absolute'] = TRUE; - } - } - else { - $options['base_url'] = $base_url; - } - } - - // The special path '' links to the default front page. - if ($path == '') { - $path = ''; - } - elseif (!empty($path) && !$options['alias']) { - $langcode = isset($options['language']) && isset($options['language']->langcode) ? $options['language']->langcode : ''; - $alias = drupal_container()->get('path.alias_manager')->getPathAlias($original_path, $langcode); - if ($alias != $original_path) { - $path = $alias; - } - } - - $base = $options['absolute'] ? $options['base_url'] . '/' : base_path(); - $prefix = empty($path) ? rtrim($options['prefix'], '/') : $options['prefix']; - - $path = drupal_encode_path($prefix . $path); - $query = $options['query'] ? ('?' . drupal_http_build_query($options['query'])) : ''; - return $base . $options['script'] . $path . $query . $options['fragment']; + return drupal_container()->get('router.generator')->generateFromPath($path, $options); } + /** * Returns TRUE if a path is external to Drupal (e.g. http://example.com). * diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index 035f282..14ad04b 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -10,6 +10,7 @@ use Drupal\Core\DependencyInjection\Compiler\RegisterKernelListenersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterAccessChecksPass; use Drupal\Core\DependencyInjection\Compiler\RegisterMatchersPass; +use Drupal\Core\DependencyInjection\Compiler\RegisterPathProcessorsPass; use Drupal\Core\DependencyInjection\Compiler\RegisterRouteFiltersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterRouteEnhancersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterParamConvertersPass; @@ -239,6 +240,7 @@ public function build(ContainerBuilder $container) { ->addTag('event_subscriber'); $container->register('path_subscriber', 'Drupal\Core\EventSubscriber\PathSubscriber') ->addArgument(new Reference('path.alias_manager.cached')) + ->addArgument(new Reference('path_processor_manager')) ->addTag('event_subscriber'); $container->register('legacy_request_subscriber', 'Drupal\Core\EventSubscriber\LegacyRequestSubscriber') ->addTag('event_subscriber'); @@ -264,6 +266,8 @@ public function build(ContainerBuilder $container) { ->addTag('event_subscriber') ->addArgument(array(new Reference('exception_controller'), 'execute')); + $this->registerPathProcessors($container); + $container ->register('transliteration', 'Drupal\Core\Transliteration\PHPTransliteration'); @@ -318,7 +322,7 @@ protected function registerRouting(ContainerBuilder $container) { ->addMethodCall('setFinalMatcher', array(new Reference('router.matcher.final_matcher'))); $container->register('router.generator', 'Drupal\Core\Routing\UrlGenerator') ->addArgument(new Reference('router.route_provider')) - ->addArgument(new Reference('path.alias_manager.cached')); + ->addArgument(new Reference('path_processor_manager')); $container->register('router.dynamic', 'Symfony\Cmf\Component\Routing\DynamicRouter') ->addArgument(new Reference('router.request_context')) ->addArgument(new Reference('router.matcher')) @@ -367,4 +371,29 @@ protected function registerTwig(ContainerBuilder $container) { // @see http://drupal.org/node/1804998 ->addMethodCall('addExtension', array(new Definition('Twig_Extension_Debug'))); } + + /** + * Register services related to path processing. + */ + protected function registerPathProcessors(ContainerBuilder $container) { + // Register the path processor manager service. + $container->register('path_processor_manager', 'Drupal\Core\PathProcessor\PathProcessorManager'); + // Register the processor that urldecodes the path. + $container->register('path_processor_decode', 'Drupal\Core\PathProcessor\PathProcessorDecode') + ->addTag('path_processor_inbound', array('priority' => 1000)); + // Register the processor that resolves the front page. + $container->register('path_processor_front', 'Drupal\Core\PathProcessor\PathProcessorFront') + ->addArgument(new Reference('config.factory')) + ->addTag('path_processor_inbound', array('priority' => 200)) + ->addTag('path_processor_outbound', array('priority' => 200)); + // Register the alias path processor. + $container->register('path_processor_alias', 'Drupal\Core\PathProcessor\PathProcessorAlias') + ->addArgument(new Reference('path.alias_manager')) + ->addTag('path_processor_inbound', array('priority' => 100)) + ->addTag('path_processor_outbound', array('priority' => 300)); + + // Add the compiler pass that will process the tagged services. + $container->addCompilerPass(new RegisterPathProcessorsPass()); + } + } diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterPathProcessorsPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterPathProcessorsPass.php new file mode 100644 index 0000000..70de42d --- /dev/null +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterPathProcessorsPass.php @@ -0,0 +1,41 @@ +hasDefinition('path_processor_manager')) { + return; + } + $manager = $container->getDefinition('path_processor_manager'); + // Add inbound path processors. + foreach ($container->findTaggedServiceIds('path_processor_inbound') as $id => $attributes) { + $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; + $manager->addMethodCall('addInbound', array(new Reference($id), $priority)); + } + // Add outbound path processors. + foreach ($container->findTaggedServiceIds('path_processor_outbound') as $id => $attributes) { + $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; + $manager->addMethodCall('addOutbound', array(new Reference($id), $priority)); + } + } +} diff --git a/core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php index 6e5b7a9..5915d4b 100644 --- a/core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php @@ -8,6 +8,7 @@ namespace Drupal\Core\EventSubscriber; use Drupal\Core\CacheDecorator\AliasManagerCacheDecorator; +use Drupal\Core\PathProcessor\InboundPathProcessorInterface; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Event\GetResponseEvent; @@ -20,24 +21,24 @@ class PathSubscriber extends PathListenerBase implements EventSubscriberInterface { protected $aliasManager; + protected $pathProcessor; - public function __construct(AliasManagerCacheDecorator $alias_manager) { + public function __construct(AliasManagerCacheDecorator $alias_manager, InboundPathProcessorInterface $path_processor) { $this->aliasManager = $alias_manager; + $this->pathProcessor = $path_processor; } /** - * Resolve the system path. + * Converts the request path to a system path. * * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event * The Event to process. */ - public function onKernelRequestPathResolve(GetResponseEvent $event) { + public function onKernelRequestConvertPath(GetResponseEvent $event) { $request = $event->getRequest(); - $path = $this->extractPath($request); - $path = $this->aliasManager->getSystemPath($path); - $this->setPath($request, $path); - // If this is the master request, set the cache key for the caching of all - // system paths looked up during the request. + $path = trim($request->getPathInfo(), '/'); + $path = $this->pathProcessor->processInbound($path, $request); + $request->attributes->set('system_path', $path); if ($event->getRequestType() == HttpKernelInterface::MASTER_REQUEST) { $this->aliasManager->setCacheKey($path); } @@ -51,84 +52,14 @@ public function onKernelTerminate(PostResponseEvent $event) { } /** - * Resolve the front-page default path. - * - * @todo The path system should be objectified to remove the function calls in - * this method. - * - * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event - * The Event to process. - */ - public function onKernelRequestFrontPageResolve(GetResponseEvent $event) { - $request = $event->getRequest(); - $path = $this->extractPath($request); - - if (empty($path)) { - // @todo Temporary hack. Fix when configuration is injectable. - $path = config('system.site')->get('page.front'); - if (empty($path)) { - $path = 'user'; - } - } - - $this->setPath($request, $path); - } - - /** - * Decode language information embedded in the request path. - * - * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event - * The Event to process. - */ - public function onKernelRequestLanguageResolve(GetResponseEvent $event) { - // We need to act only on the master request, otherwise subrequests will - // inherit the main request path and an infinite loop will be started. - if ($event->getRequestType() == HttpKernelInterface::MASTER_REQUEST) { - $path = _language_resolved_path(); - if ($path !== NULL) { - $this->setPath($event->getRequest(), $path); - } - } - } - - /** - * Decodes the path of the request. - * - * Parameters in the URL sometimes represent code-meaningful strings. It is - * therefore useful to always urldecode() those values so that individual - * controllers need not concern themselves with it. This is Drupal-specific - * logic and may not be familiar for developers used to other Symfony-family - * projects. - * - * @todo Revisit whether or not this logic is appropriate for here or if - * controllers should be required to implement this logic themselves. If we - * decide to keep this code, remove this TODO. - * - * @param Symfony\Component\HttpKernel\Event\GetResponseEvent $event - * The Event to process. - */ - public function onKernelRequestDecodePath(GetResponseEvent $event) { - $request = $event->getRequest(); - $path = $this->extractPath($request); - - $path = urldecode($path); - - $this->setPath($request, $path); - } - - /** * Registers the methods in this class that should be listeners. * * @return array * An array of event listener definitions. */ static function getSubscribedEvents() { - $events[KernelEvents::REQUEST][] = array('onKernelRequestDecodePath', 200); - $events[KernelEvents::REQUEST][] = array('onKernelRequestLanguageResolve', 150); - $events[KernelEvents::REQUEST][] = array('onKernelRequestFrontPageResolve', 101); - $events[KernelEvents::REQUEST][] = array('onKernelRequestPathResolve', 100); + $events[KernelEvents::REQUEST][] = array('onKernelRequestConvertPath', 200); $events[KernelEvents::TERMINATE][] = array('onKernelTerminate', 200); - return $events; } } diff --git a/core/lib/Drupal/Core/PathProcessor/InboundPathProcessorInterface.php b/core/lib/Drupal/Core/PathProcessor/InboundPathProcessorInterface.php new file mode 100644 index 0000000..946b29c --- /dev/null +++ b/core/lib/Drupal/Core/PathProcessor/InboundPathProcessorInterface.php @@ -0,0 +1,28 @@ +aliasManager = $alias_manager; + } + + /** + * Implements Drupal\Core\PathProcessor\InboundPathProcessorInterface::processInbound(). + */ + public function processInbound($path, Request $request) { + $path = $this->aliasManager->getSystemPath($path); + return $path; + } + + /** + * Implements Drupal\Core\PathProcessor\OutboundPathProcessorInterface::processOutbound(). + */ + public function processOutbound($path, $options = array()) { + $path = $this->aliasManager->getPathAlias($path); + return $path; + } +} diff --git a/core/lib/Drupal/Core/PathProcessor/PathProcessorDecode.php b/core/lib/Drupal/Core/PathProcessor/PathProcessorDecode.php new file mode 100644 index 0000000..0674cda --- /dev/null +++ b/core/lib/Drupal/Core/PathProcessor/PathProcessorDecode.php @@ -0,0 +1,35 @@ +config = $config; + } + + /** + * Implements Drupal\Core\PathProcessor\InboundPathProcessorInterface::processInbound(). + */ + public function processInbound($path, Request $request) { + if (empty($path)) { + $path = $this->config->get('system.site')->get('page.front'); + if (empty($path)) { + $path = 'user'; + } + } + return $path; + } + + /** + * Implements Drupal\Core\PathProcessor\OutboundPathProcessorInterface::processOutbound(). + */ + public function processOutbound($path, $options = array()) { + // The special path '' links to the default front page. + if ($path == '') { + $path = ''; + } + return $path; + } + +} diff --git a/core/lib/Drupal/Core/PathProcessor/PathProcessorManager.php b/core/lib/Drupal/Core/PathProcessor/PathProcessorManager.php new file mode 100644 index 0000000..142a3ba --- /dev/null +++ b/core/lib/Drupal/Core/PathProcessor/PathProcessorManager.php @@ -0,0 +1,149 @@ +inboundProcessors[$priority][] = $processor; + $this->sortedInbound = array(); + } + + /** + * Implements Drupal\Core\PathProcessor\InboundPathProcessorInterface::processInbound(). + */ + public function processInbound($path, Request $request) { + $processors = $this->getInbound(); + foreach ($processors as $processor) { + $path = $processor->processInbound($path, $request); + } + return $path; + } + + /** + * Returns the sorted array of inbound processors. + * + * @return array + * An array of processor objects. + */ + protected function getInbound() { + if (empty($this->sortedInbound)) { + $this->sortedInbound = $this->sortProcessors('inboundProcessors'); + } + + return $this->sortedInbound; + } + + + /** + * Adds an outbound processor object to the $outboundProcessors property. + * + * @param \Drupal\Core\PathProcessor\OutboundPathProcessorInterface $processor + * The processor object to add. + * + * @param int $priority + * The priority of the processor being added. + */ + public function addOutbound(OutboundPathProcessorInterface $processor, $priority = 0) { + $this->outboundProcessors[$priority][] = $processor; + $this->sortedOutbound = array(); + } + + /** + * Implements Drupal\Core\PathProcessor\OutboundPathProcessorInterface::processOutbound(). + */ + public function processOutbound($path, $options = array()) { + $processors = $this->getOutbound(); + foreach ($processors as $processor) { + $path = $processor->processOutbound($path, $options); + } + return $path; + } + + /** + * Returns the sorted array of outbound processors. + * + * @return array + * An array of processor objects. + */ + protected function getOutbound() { + if (empty($this->sortedOutbound)) { + $this->sortedOutbound = $this->sortProcessors('outboundProcessors'); + } + + return $this->sortedOutbound; + } + + /** + * Sorts the processors according to priority. + * + * @param string $type + * The processor type to sort, e.g. 'inboundProcessors'. + */ + protected function sortProcessors($type) { + $sorted = array(); + krsort($this->{$type}); + + foreach ($this->{$type} as $processors) { + $sorted = array_merge($sorted, $processors); + } + return $sorted; + } +} diff --git a/core/lib/Drupal/Core/Routing/PathBasedGeneratorInterface.php b/core/lib/Drupal/Core/Routing/PathBasedGeneratorInterface.php new file mode 100644 index 0000000..16b258b --- /dev/null +++ b/core/lib/Drupal/Core/Routing/PathBasedGeneratorInterface.php @@ -0,0 +1,75 @@ +'. A few notes: + * - If you provide a full URL, it will be considered an external URL. + * - If you provide only the path (e.g. "node/34"), it will be + * considered an internal link. In this case, it should be a system URL, + * and it will be replaced with the alias, if one exists. Additional query + * arguments for internal paths must be supplied in $options['query'], not + * included in $path. + * - If you provide an internal path and $options['alias'] is set to TRUE, the + * path is assumed already to be the correct path alias, and the alias is + * not looked up. + * - The special string '' generates a link to the site's base URL. + * - If your external URL contains a query (e.g. http://example.com/foo?a=b), + * then you can either URL encode the query keys and values yourself and + * include them in $path, or use $options['query'] to let this function + * URL encode them. + * @param $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. + * - '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. + * - 'alias': Defaults to FALSE. Whether the given path is a URL alias + * already. + * - 'external': Whether the given path is an external URL. + * - 'language': An optional language object. If the path being linked to is + * internal to the site, $options['language'] is 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. TRUE enforces HTTPS and FALSE enforces HTTP, but HTTPS can + * only be enforced when the variable 'https' is set to TRUE. + * - 'base_url': Only used internally, to modify the base URL when a language + * dependent URL requires so. + * - 'prefix': Only used internally, to modify the path when a language + * dependent URL requires so. + * - 'script': Added to the URL between the base path and the path prefix. + * Defaults to empty string when clean URLs are in effect, and to + * 'index.php/' when they are not. + * - 'entity_type': The entity type of the object that called url(). Only + * set if url() is invoked by Drupal\Core\Entity\Entity::uri(). + * - 'entity': The entity object (such as a node) for which the URL is being + * generated. Only set if url() is invoked by Drupal\Core\Entity\Entity::uri(). + * + * @return + * A string containing a URL to the given path. + */ + public function generateFromPath($path = NULL, $options = array()); + +} diff --git a/core/lib/Drupal/Core/Routing/UrlGenerator.php b/core/lib/Drupal/Core/Routing/UrlGenerator.php index 75779a2..0d32d79 100644 --- a/core/lib/Drupal/Core/Routing/UrlGenerator.php +++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php @@ -12,19 +12,19 @@ use Symfony\Cmf\Component\Routing\ProviderBasedGenerator; use Symfony\Cmf\Component\Routing\RouteProviderInterface; -use Drupal\Core\Path\AliasManagerInterface; +use Drupal\Core\PathProcessor\OutboundPathProcessorInterface; /** * A Generator creates URL strings based on a specified route. */ -class UrlGenerator extends ProviderBasedGenerator { +class UrlGenerator extends ProviderBasedGenerator implements PathBasedGeneratorInterface { /** - * The alias manager that will be used to alias generated URLs. + * The path processor to convert the system path to one suitable for urls. * - * @var AliasManagerInterface + * @var \Drupal\Core\PathProcessor\OutboundPathProcessorInterface */ - protected $aliasManager; + protected $pathProcessor; /** * Constructs a new generator object. @@ -36,23 +36,114 @@ class UrlGenerator extends ProviderBasedGenerator { * @param \Symfony\Component\HttpKernel\Log\LoggerInterface $logger * An optional logger for recording errors. */ - public function __construct(RouteProviderInterface $provider, AliasManagerInterface $alias_manager, LoggerInterface $logger = NULL) { + public function __construct(RouteProviderInterface $provider, OutboundPathProcessorInterface $path_processor, LoggerInterface $logger = NULL) { parent::__construct($provider, $logger); - $this->aliasManager = $alias_manager; + $this->pathProcessor = $path_processor; } /** - * Implements Symfony\Component\Routing\Generator\UrlGeneratorInterface::generate(); + * Implements Symfony\Component\Routing\Generator\UrlGeneratorInterface::generate(). */ public function generate($name, $parameters = array(), $absolute = FALSE) { $path = parent::generate($name, $parameters, $absolute); + $path = $this->processPath($path); - // This method is expected to return a path with a leading /, whereas - // the alias manager has no leading /. - $path = '/' . $this->aliasManager->getPathAlias(trim($path, '/')); + return $path; + } + /** + * Passes the path to a processor manager to allow alterations. + */ + protected function processPath($path, $options = array()) { + $path_parts = explode('?', $path); + $path = '/' . $this->pathProcessor->processOutbound(trim($path_parts[0], '/'), $options); + if (isset($path_parts[1])) { + $path .= '?' . $path_parts[1]; + } return $path; } + /** + * Implements \Drupal\Core\Routing\PathBasedGeneratorInterface::generateFromPath(). + */ + public function generateFromPath($path = NULL, $options = array()) { + // Merge in defaults. + $options += array( + 'fragment' => '', + 'query' => array(), + 'absolute' => FALSE, + 'prefix' => '', + 'script' => $GLOBALS['script_path'], + ); + + if (!isset($options['external'])) { + // Return an external link if $path contains an allowed absolute URL. Only + // call the slow drupal_strip_dangerous_protocols() if $path contains a ':' + // before any / ? or #. Note: we could use url_is_external($path) here, but + // that would require another function call, and performance inside url() is + // critical. + $colonpos = strpos($path, ':'); + $options['external'] = ($colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && drupal_strip_dangerous_protocols($path) == $path); + } + + if (isset($options['fragment']) && $options['fragment'] !== '') { + $options['fragment'] = '#' . $options['fragment']; + } + + if ($options['external']) { + // Split off the fragment. + if (strpos($path, '#') !== FALSE) { + list($path, $old_fragment) = explode('#', $path, 2); + // If $options contains no fragment, take it over from the path. + if (isset($old_fragment) && !$options['fragment']) { + $options['fragment'] = '#' . $old_fragment; + } + } + // Append the query. + if ($options['query']) { + $path .= (strpos($path, '?') !== FALSE ? '&' : '?') . drupal_http_build_query($options['query']); + } + if (isset($options['https']) && variable_get('https', FALSE)) { + if ($options['https'] === TRUE) { + $path = str_replace('http://', 'https://', $path); + } + elseif ($options['https'] === FALSE) { + $path = str_replace('https://', 'http://', $path); + } + } + // Reassemble. + return $path . $options['fragment']; + } + else { + $path = ltrim($this->processPath($path, $options), '/'); + } + + global $base_url, $base_secure_url, $base_insecure_url; + + // The base_url might be rewritten from the language rewrite in domain mode. + if (!isset($options['base_url'])) { + if (isset($options['https']) && variable_get('https', FALSE)) { + if ($options['https'] === TRUE) { + $options['base_url'] = $base_secure_url; + $options['absolute'] = TRUE; + } + elseif ($options['https'] === FALSE) { + $options['base_url'] = $base_insecure_url; + $options['absolute'] = TRUE; + } + } + else { + $options['base_url'] = $base_url; + } + } + + $base = $options['absolute'] ? $options['base_url'] . '/' : base_path(); + $prefix = empty($path) ? rtrim($options['prefix'], '/') : $options['prefix']; + + $path = drupal_encode_path($prefix . $path); + $query = $options['query'] ? ('?' . drupal_http_build_query($options['query'])) : ''; + return $base . $options['script'] . $path . $query . $options['fragment']; + } + } diff --git a/core/modules/language/language.negotiation.inc b/core/modules/language/language.negotiation.inc index 9521247..d4cfdbb 100644 --- a/core/modules/language/language.negotiation.inc +++ b/core/modules/language/language.negotiation.inc @@ -282,11 +282,7 @@ function language_from_url($languages, Request $request = NULL) { case LANGUAGE_NEGOTIATION_URL_PREFIX: $request_path = urldecode(trim($request->getPathInfo(), '/')); - list($language, $path) = language_url_split_prefix($request_path, $languages); - // Store the correct system path, i.e., the request path without the - // language prefix. - _language_resolved_path($path); if ($language !== FALSE) { $language_url = $language->langcode; @@ -452,55 +448,45 @@ function language_url_rewrite_url(&$path, &$options) { return; } - if (isset($options['language'])) { - switch (config('language.negotiation')->get('url.source')) { - case LANGUAGE_NEGOTIATION_URL_DOMAIN: - $domains = language_negotiation_url_domains(); - if (is_object($options['language']) && !empty($domains[$options['language']->langcode])) { - global $is_https; + if (isset($options['language']) && config('language.negotiation')->get('url.source') == LANGUAGE_NEGOTIATION_URL_DOMAIN) { - // Save the original base URL. If it contains a port, we need to - // retain it below. - if (!empty($options['base_url'])) { - // The colon in the URL scheme messes up the port checking below. - $normalized_base_url = str_replace(array('https://', 'http://'), '', $options['base_url']); + $domains = language_negotiation_url_domains(); + if (is_object($options['language']) && !empty($domains[$options['language']->langcode])) { + global $is_https; - } + // Save the original base URL. If it contains a port, we need to + // retain it below. + if (!empty($options['base_url'])) { + // The colon in the URL scheme messes up the port checking below. + $normalized_base_url = str_replace(array('https://', 'http://'), '', $options['base_url']); - // Ask for an absolute URL with our modified base URL. - $url_scheme = ($is_https) ? 'https://' : 'http://'; - $options['absolute'] = TRUE; - $options['base_url'] = $url_scheme . $domains[$options['language']->langcode]; - - // In case either the original base URL or the HTTP host contains a - // port, retain it. - $http_host = $_SERVER['HTTP_HOST']; - if (isset($normalized_base_url) && strpos($normalized_base_url, ':') !== FALSE) { - list($host, $port) = explode(':', $normalized_base_url); - $options['base_url'] .= ':' . $port; - } - elseif (strpos($http_host, ':') !== FALSE) { - list($host, $port) = explode(':', $http_host); - $options['base_url'] .= ':' . $port; - } + } - if (isset($options['https']) && variable_get('https', FALSE)) { - if ($options['https'] === TRUE) { - $options['base_url'] = str_replace('http://', 'https://', $options['base_url']); - } - elseif ($options['https'] === FALSE) { - $options['base_url'] = str_replace('https://', 'http://', $options['base_url']); - } - } - } - break; + // Ask for an absolute URL with our modified base URL. + $url_scheme = ($is_https) ? 'https://' : 'http://'; + $options['absolute'] = TRUE; + $options['base_url'] = $url_scheme . $domains[$options['language']->langcode]; + + // In case either the original base URL or the HTTP host contains a + // port, retain it. + $http_host = $_SERVER['HTTP_HOST']; + if (isset($normalized_base_url) && strpos($normalized_base_url, ':') !== FALSE) { + list($host, $port) = explode(':', $normalized_base_url); + $options['base_url'] .= ':' . $port; + } + elseif (strpos($http_host, ':') !== FALSE) { + list($host, $port) = explode(':', $http_host); + $options['base_url'] .= ':' . $port; + } - case LANGUAGE_NEGOTIATION_URL_PREFIX: - $prefixes = language_negotiation_url_prefixes(); - if (is_object($options['language']) &&!empty($prefixes[$options['language']->langcode])) { - $options['prefix'] = $prefixes[$options['language']->langcode] . '/'; + if (isset($options['https']) && variable_get('https', FALSE)) { + if ($options['https'] === TRUE) { + $options['base_url'] = str_replace('http://', 'https://', $options['base_url']); } - break; + elseif ($options['https'] === FALSE) { + $options['base_url'] = str_replace('https://', 'http://', $options['base_url']); + } + } } } } diff --git a/core/modules/language/lib/Drupal/language/HttpKernel/PathProcessorLanguage.php b/core/modules/language/lib/Drupal/language/HttpKernel/PathProcessorLanguage.php new file mode 100644 index 0000000..8287571 --- /dev/null +++ b/core/modules/language/lib/Drupal/language/HttpKernel/PathProcessorLanguage.php @@ -0,0 +1,86 @@ +config = $config; + $this->languageManager = $language_manager; + $this->languages = language_list(); + } + + /** + * Implements Drupal\Core\PathProcessor\InboundPathProcessorInterface::processInbound(). + */ + public function processInbound($path, Request $request) { + if (!empty($path)) { + $args = explode('/', $path); + $prefix = array_shift($args); + + // Search prefix within enabled languages. + $prefixes = $this->config->get('language.negotiation')->get('url.prefixes'); + foreach ($this->languages as $language) { + if (isset($prefixes[$language->langcode]) && $prefixes[$language->langcode] == $prefix) { + // Rebuild $path with the language removed. + return implode('/', $args); + } + } + } + return $path; + } + + /** + * Implements Drupal\Core\PathProcessor\InboundPathProcessorInterface::processOutbound(). + */ + public function processOutbound($path, $options = array()) { + $languages = array_flip(array_keys($this->languages)); + // @todo Go back to using a constant instead of the string 'path_prefix' once we can use a class + // constant. + if ($this->config->get('language.negotiation')->get('url.source') == 'path_prefix') { + // Language can be passed as an option, or we go for current URL language. + if (!isset($options['language'])) { + $language_url = $this->languageManager->getLanguage(LANGUAGE_TYPE_URL); + $options['language'] = $language_url; + } + // We allow only enabled languages here. + elseif (is_object($options['language']) && !isset($languages[$options['language']->langcode])) { + return $path; + } + $prefixes = $this->config->get('language.negotiation')->get('url.prefixes'); + if (is_object($options['language']) && !empty($prefixes[$options['language']->langcode])) { + return $prefixes[$options['language']->langcode] . '/' . $path; + } + } + return $path; + } +} diff --git a/core/modules/language/lib/Drupal/language/LanguageBundle.php b/core/modules/language/lib/Drupal/language/LanguageBundle.php new file mode 100644 index 0000000..a0a76f5 --- /dev/null +++ b/core/modules/language/lib/Drupal/language/LanguageBundle.php @@ -0,0 +1,31 @@ +register('path_processor_language', 'Drupal\language\HttpKernel\PathProcessorLanguage') + ->addArgument(new Reference('config.factory')) + ->addArgument(new Reference('language_manager')) + ->addTag('path_processor_inbound', array('priority' => 300)) + ->addTag('path_processor_outbound', array('priority' => 100)); + } + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Path/AliasTest.php b/core/modules/system/lib/Drupal/system/Tests/Path/AliasTest.php index 275e1c4..5dc3158 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Path/AliasTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Path/AliasTest.php @@ -2,7 +2,7 @@ /** * @file - * Definition of Drupal\system\Tests\Path\CrudTest. + * Contains Drupal\system\Tests\Path\AliasTest. */ namespace Drupal\system\Tests\Path; @@ -15,7 +15,7 @@ /** * Tests path alias CRUD and lookup functionality. */ -class AliasTest extends DrupalUnitTestBase { +class AliasTest extends PathUnitTestBase { public static function getInfo() { return array( @@ -25,18 +25,6 @@ public static function getInfo() { ); } - public function setUp() { - parent::setUp(); - $this->fixtures = new UrlAliasFixtures(); - } - - public function tearDown() { - $this->fixtures->dropTables(Database::getConnection()); - - parent::tearDown(); - } - - function testCRUD() { //Prepare database table. $connection = Database::getConnection(); diff --git a/core/modules/system/lib/Drupal/system/Tests/Path/PathUnitTestBase.php b/core/modules/system/lib/Drupal/system/Tests/Path/PathUnitTestBase.php new file mode 100644 index 0000000..c24506d --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Path/PathUnitTestBase.php @@ -0,0 +1,28 @@ +fixtures = new UrlAliasFixtures(); + } + + public function tearDown() { + $this->fixtures->dropTables(Database::getConnection()); + + parent::tearDown(); + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Path/UrlAliasFixtures.php b/core/modules/system/lib/Drupal/system/Tests/Path/UrlAliasFixtures.php index 6fb02da..a51e3bf 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Path/UrlAliasFixtures.php +++ b/core/modules/system/lib/Drupal/system/Tests/Path/UrlAliasFixtures.php @@ -16,7 +16,7 @@ class UrlAliasFixtures { * The connection to use to create the tables. */ public function createTables(Connection $connection) { - $tables = $this->urlAliasTableDefinition(); + $tables = $this->tableDefinition(); $schema = $connection->schema(); foreach ($tables as $name => $table) { @@ -32,7 +32,7 @@ public function createTables(Connection $connection) { * The connection to use to drop the tables. */ public function dropTables(Connection $connection) { - $tables = $this->urlAliasTableDefinition(); + $tables = $this->tableDefinition(); $schema = $connection->schema(); foreach ($tables as $name => $table) { @@ -77,7 +77,7 @@ public function sampleUrlAliases() { * @return array * Table definitions. */ - public function urlAliasTableDefinition() { + public function tableDefinition() { $tables = array(); module_load_install('system'); diff --git a/core/modules/system/lib/Drupal/system/Tests/PathProcessor/PathProcessorFixtures.php b/core/modules/system/lib/Drupal/system/Tests/PathProcessor/PathProcessorFixtures.php new file mode 100644 index 0000000..dbc987d --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/PathProcessor/PathProcessorFixtures.php @@ -0,0 +1,27 @@ + t('Path Processor Unit Tests'), + 'description' => t('Tests processing of the inbound path.'), + 'group' => t('Path API'), + ); + } + + public function setUp() { + parent::setUp(); + $this->fixtures = new PathProcessorFixtures(); + } + + /** + * Tests resolving the inbound path to the system path. + */ + function testProcessInbound() { + + // Ensure all tables needed for these tests are created. + $connection = Database::getConnection(); + $this->fixtures->createTables($connection); + + // Create dependecies needed by various path processors. + $language_manager = $this->container->get('language_manager'); + $alias_manager = new AliasManager($connection, $this->container->get('state'), $language_manager); + $module_handler = $this->container->get('module_handler'); + $config = $this->container->get('config.factory'); + + // Add a url alias for testing the alias-based processor. + $path_crud = new Path($connection, $alias_manager); + $path_crud->save('user/1', 'foo'); + + // Add a language for testing the language-based processor. + $module_handler->setModuleList(array('language' => 'core/modules/language/language.module')); + $module_handler->load('language'); + $language = new \stdClass(); + $language->langcode = 'fr'; + $language->name = 'French'; + language_save($language); + + // Create the processors. + $alias_processor = new PathProcessorAlias($alias_manager); + $decode_processor = new PathProcessorDecode(); + $front_processor = new PathProcessorFront($config); + $language_processor = new PathProcessorLanguage($config, $language_manager); + + // First, test the processor manager with the processors in the incorrect + // order. The alias processor will run before the language processor, meaning + // aliases will not be found. + $priorities = array( + 1000 => $alias_processor, + 500 => $decode_processor, + 300 => $front_processor, + 200 => $language_processor, + ); + + // Create the processor manager and add the processors. + $processor_manager = new PathProcessorManager(); + foreach ($priorities as $priority => $processor) { + $processor_manager->addInbound($processor, $priority); + } + + // Test resolving the French homepage using the incorrect processor order. + $test_path = 'fr'; + $request = Request::create($test_path); + $processed = $processor_manager->processInbound($test_path, $request); + $this->assertEqual($processed, '', 'Processing in the incorrect order fails to resolve the system path from the empty path'); + + // Test resolving an existing alias using the incorrect processor order. + $test_path = 'fr/foo'; + $request = Request::create($test_path); + $processed = $processor_manager->processInbound($test_path, $request); + $this->assertEqual($processed, 'foo', 'Processing in the incorrect order fails to resolve the system path from an alias'); + + // Now create a new processor manager and add the processors, this time in + // the correct order. + $processor_manager = new PathProcessorManager(); + $priorities = array( + 1000 => $decode_processor, + 500 => $language_processor, + 300 => $front_processor, + 200 => $alias_processor, + ); + foreach ($priorities as $priority => $processor) { + $processor_manager->addInbound($processor, $priority); + } + + // Test resolving the French homepage using the correct processor order. + $test_path = 'fr'; + $request = Request::create($test_path); + $processed = $processor_manager->processInbound($test_path, $request); + $this->assertEqual($processed, 'user', 'Processing in the correct order resolves the system path from the empty path.'); + + // Test resolving an existing alias using the correct processor order. + $test_path = 'fr/foo'; + $request = Request::create($test_path); + $processed = $processor_manager->processInbound($test_path, $request); + $this->assertEqual($processed, 'user/1', 'Processing in the correct order resolves the system path from an alias.'); + } + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/UrlGeneratorTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/UrlGeneratorTest.php index 0e9f13e..5ca0db6 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Routing/UrlGeneratorTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Routing/UrlGeneratorTest.php @@ -7,6 +7,8 @@ namespace Drupal\system\Tests\Routing; +use Drupal\Core\PathProcessor\PathProcessorAlias; +use Drupal\Core\PathProcessor\PathProcessorManager; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; @@ -47,7 +49,10 @@ function setUp() { $context = new RequestContext(); $context->fromRequest(Request::create('/some/path')); - $generator = new UrlGenerator($provider, $this->aliasManager); + $processor = new PathProcessorAlias($this->aliasManager); + $processor_manager = new PathProcessorManager(); + $processor_manager->addOutbound($processor, 1000); + $generator = new UrlGenerator($provider, $processor_manager); $generator->setContext($context); $this->generator = $generator; @@ -67,10 +72,8 @@ public function testAliasGeneration() { */ public function testAliasGenerationWithParameters() { $this->aliasManager->addAlias('test/two/5', 'goodbye/cruel/world'); - $url = $this->generator->generate('test_2', array('narf' => '5')); - - $this->assertEqual($url, '/goodbye/cruel/world', 'Correct URL generated including alias and parameters.'); + $this->assertEqual($url, '/goodbye/cruel/world?other=stuff', 'Correct URL generated including alias and parameters.'); } }