From b21943922d94261dfa8de34995d206d5e8e9c3d7 Mon Sep 17 00:00:00 2001 From: Alex Pott <alex.a.pott@googlemail.com> Date: Thu, 6 Jun 2013 09:14:16 +0100 Subject: [PATCH] Issue #1888424 by katbailey, steveoliver, twistor, beejeebus, effulgentsia: Change notice: Make Drupal's URL generation logic available to HttpKernel, and minimize code repetition/divergence. --- core/core.services.yml | 12 +- core/includes/common.inc | 203 ++-------- core/includes/form.inc | 3 +- core/includes/install.core.inc | 11 +- core/lib/Drupal.php | 10 + .../Compiler/RegisterPathProcessorsPass.php | 6 + .../Core/EventSubscriber/PathSubscriber.php | 20 + .../Drupal/Core/Language/LanguageManager.php | 2 +- .../OutboundPathProcessorInterface.php | 35 ++ .../Core/PathProcessor/PathProcessorAlias.php | 10 +- .../Core/PathProcessor/PathProcessorFront.php | 13 +- .../PathProcessor/PathProcessorManager.php | 65 +++- .../GeneratorNotInitializedException.php | 14 + .../lib/Drupal/Core/Routing/NullGenerator.php | 20 +- .../Routing/PathBasedGeneratorInterface.php | 93 +++++ core/lib/Drupal/Core/Routing/UrlGenerator.php | 367 +++++++++++++++++- .../contextual/Tests/ContextualUnitTest.php | 4 +- .../lib/Drupal/file/Tests/DownloadTest.php | 19 +- .../lib/Drupal/forum/Tests/ForumTest.php | 2 +- core/modules/image/image.module | 17 +- .../image/Tests/ImageStylesPathAndUrlTest.php | 18 +- core/modules/language/language.module | 51 --- .../modules/language/language.negotiation.inc | 77 ---- core/modules/language/language.services.yml | 3 +- .../HttpKernel/PathProcessorLanguage.php | 95 ++++- .../LanguageUILanguageNegotiationTest.php | 9 +- .../Tests/LanguageUrlRewritingTest.php | 20 +- .../tests/language_test/language_test.module | 15 +- core/modules/locale/locale.install | 4 + .../Drupal/path/Tests/PathLanguageTest.php | 6 +- .../Drupal/rdf/SiteSchema/BundleSchema.php | 2 +- .../Drupal/rdf/SiteSchema/EntitySchema.php | 2 +- .../lib/Drupal/rdf/Tests/SiteSchemaTest.php | 1 - .../Drupal/simpletest/DrupalUnitTestBase.php | 10 + .../lib/Drupal/simpletest/WebTestBase.php | 77 +++- .../Drupal/system/Tests/Common/UrlTest.php | 45 +-- .../Tests/Path/UrlAlterFunctionalTest.php | 5 +- .../system/Tests/Routing/UrlGeneratorTest.php | 76 ---- .../Tests/Upgrade/LanguageUpgradePathTest.php | 5 + .../Tests/Upgrade/UpgradePathTestBase.php | 34 ++ core/modules/system/tests/https.php | 3 + .../url_alter_test/PathProcessorTest.php | 61 +++ .../url_alter_test/url_alter_test.module | 18 - .../url_alter_test.services.yml | 5 +- .../translation/Tests/TranslationTest.php | 3 +- .../Tests/EntityTranslationTestBase.php | 4 + .../Tests/EntityTranslationWorkflowsTest.php | 1 + core/scripts/run-tests.sh | 6 +- .../Core/PathProcessor/PathProcessorTest.php | 11 +- .../Tests/Core/Routing/UrlGeneratorTest.php | 167 ++++++++ 50 files changed, 1222 insertions(+), 538 deletions(-) create mode 100644 core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php create mode 100644 core/lib/Drupal/Core/Routing/GeneratorNotInitializedException.php create mode 100644 core/lib/Drupal/Core/Routing/PathBasedGeneratorInterface.php delete mode 100644 core/modules/system/lib/Drupal/system/Tests/Routing/UrlGeneratorTest.php create mode 100644 core/modules/system/tests/modules/url_alter_test/lib/Drupal/url_alter_test/PathProcessorTest.php create mode 100644 core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php diff --git a/core/core.services.yml b/core/core.services.yml index 856c1982d9c4..a54bb4a50a4e 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -198,12 +198,16 @@ services: arguments: ['@router.route_provider'] calls: - [setFinalMatcher, ['@router.matcher.final_matcher']] - router.generator: + url_generator: class: Drupal\Core\Routing\UrlGenerator - arguments: ['@router.route_provider', '@path.alias_manager.cached'] + arguments: ['@router.route_provider', '@path_processor_manager', '@config.factory', '@settings'] + calls: + - [setRequest, ['@?request']] + tags: + - { name: persist } router.dynamic: class: Symfony\Cmf\Component\Routing\DynamicRouter - arguments: ['@router.request_context', '@router.matcher', '@router.generator'] + arguments: ['@router.request_context', '@router.matcher', '@url_generator'] legacy_generator: class: Drupal\Core\Routing\NullGenerator legacy_url_matcher: @@ -390,11 +394,13 @@ services: class: Drupal\Core\PathProcessor\PathProcessorFront tags: - { name: path_processor_inbound, priority: 200 } + - { name: path_processor_outbound, priority: 200 } arguments: ['@config.factory'] path_processor_alias: class: Drupal\Core\PathProcessor\PathProcessorAlias tags: - { name: path_processor_inbound, priority: 100 } + - { name: path_processor_outbound, priority: 300 } arguments: ['@path.alias_manager'] transliteration: class: Drupal\Core\Transliteration\PHPTransliteration diff --git a/core/includes/common.inc b/core/includes/common.inc index a8ad43a1bba3..0f8a90ea0bf3 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -13,6 +13,7 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Database\Database; +use Drupal\Core\Routing\GeneratorNotInitializedException; use Drupal\Core\SystemListingInfo; use Drupal\Core\Template\Attribute; @@ -498,42 +499,14 @@ function drupal_get_query_array($query) { /** * Parses an array into a valid, rawurlencoded query string. * - * This differs from http_build_query() as we need to rawurlencode() (instead of - * urlencode()) all query parameters. - * - * @param $query - * The query parameter array to be processed, e.g. $_GET. - * @param $parent - * Internal use only. Used to build the $query array key for nested items. - * - * @return - * A rawurlencoded string which can be used as or appended to the URL query - * string. - * + * @see \Drupal\Core\Routing\PathBasedGeneratorInterface::httpBuildQuery() * @see drupal_get_query_parameters() + * @deprecated as of Drupal 8.0. Use + * Drupal::urlGenerator()->httpBuildQuery() instead. * @ingroup php_wrappers */ function drupal_http_build_query(array $query, $parent = '') { - $params = array(); - - foreach ($query as $key => $value) { - $key = ($parent ? $parent . '[' . rawurlencode($key) . ']' : rawurlencode($key)); - - // Recurse into children. - if (is_array($value)) { - $params[] = drupal_http_build_query($value, $key); - } - // If a query parameter value is NULL, only append its key. - elseif (!isset($value)) { - $params[] = $key; - } - else { - // For better readability of paths in query strings, we decode slashes. - $params[] = $key . '=' . str_replace('%2F', '/', rawurlencode($value)); - } - } - - return implode('&', $params); + return Drupal::urlGenerator()->httpBuildQuery($query, $parent); } /** @@ -564,7 +537,7 @@ function drupal_get_destination() { } else { $path = current_path(); - $query = drupal_http_build_query(drupal_get_query_parameters()); + $query = Drupal::urlGenerator()->httpBuildQuery(drupal_get_query_parameters()); if ($query != '') { $path .= '?' . $query; } @@ -725,8 +698,7 @@ function drupal_goto($path = '', array $options = array(), $http_response_code = // The 'Location' HTTP header must be absolute. $options['absolute'] = TRUE; - $url = url($path, $options); - + $url = Drupal::urlGenerator()->generateFromPath($path, $options); header('Location: ' . $url, TRUE, $http_response_code); // The "Location" header sends a redirect status code to the HTTP daemon. In @@ -1423,153 +1395,24 @@ function datetime_default_format_type() { * 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 '<front>'. 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 '<front>' 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']) && settings()->get('mixed_mode_sessions', 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']) && settings()->get('mixed_mode_sessions', 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 '<front>' links to the default front page. - if ($path == '<front>') { - $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']; + $generator = Drupal::urlGenerator(); + try { + $url = $generator->generateFromPath($path, $options); + } + catch (GeneratorNotInitializedException $e) { + // Fallback to using globals. + // @todo Remove this once there is no code that calls url() when there is + // no request. + global $base_url, $base_path, $script_path; + $generator->setBasePath($base_path); + $generator->setBaseUrl($base_url . '/'); + $generator->setScriptPath($script_path); + $url = $generator->generateFromPath($path, $options); + } + return $url; } /** diff --git a/core/includes/form.inc b/core/includes/form.inc index 36e165449f62..b3286259dcb6 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -1320,7 +1320,8 @@ function drupal_redirect_form($form_state) { $function($form_state['redirect']); } } - drupal_goto(current_path(), array('query' => drupal_container()->get('request')->query->all())); + $request = Drupal::request(); + drupal_goto($request->attributes->get('system_path'), array('query' => $request->query->all())); } } diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 686facbca6cd..225def291fa7 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -410,6 +410,8 @@ function install_begin_request(&$install_state) { // Register Twig template engine for use during install. CoreBundle::registerTwig($container); + $container->register('url_generator', 'Drupal\Core\Routing\NullGenerator'); + Drupal::setContainer($container); } @@ -1948,7 +1950,14 @@ function install_finished(&$install_state) { $messages = drupal_set_message(); $output = '<p>' . st('Congratulations, you installed @drupal!', array('@drupal' => drupal_install_profile_distribution_name())) . '</p>'; - $output .= '<p>' . (isset($messages['error']) ? st('Review the messages above before visiting <a href="@url">your new site</a>.', array('@url' => url(''))) : st('<a href="@url">Visit your new site</a>.', array('@url' => url('')))) . '</p>'; + // Ensure the URL that is generated for the home page does not have 'install.php' + // in it. + $request = Request::createFromGlobals(); + $generator = Drupal::urlGenerator(); + $generator->setBasePath(str_replace('/core', '', $request->getBasePath()) . '/'); + $generator->setScriptPath(''); + $url = $generator->generateFromPath(''); + $output .= '<p>' . (isset($messages['error']) ? st('Review the messages above before visiting <a href="@url">your new site</a>.', array('@url' => $url)) : st('<a href="@url">Visit your new site</a>.', array('@url' => $url))) . '</p>'; // Run cron to populate update status tables (if available) so that users // will be warned if they've installed an out of date Drupal version. diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index d8a68badec7f..1b3dd4c43dcb 100644 --- a/core/lib/Drupal.php +++ b/core/lib/Drupal.php @@ -361,4 +361,14 @@ public static function token() { return static::$container->get('token'); } + /** + * Returns the url generator service. + * + * @return \Drupal\Core\Routing\UrlGenerator + * The url generator service. + */ + public static function urlGenerator() { + return static::$container->get('url_generator'); + } + } diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterPathProcessorsPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterPathProcessorsPass.php index 6e298da910da..70de42da3815 100644 --- a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterPathProcessorsPass.php +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterPathProcessorsPass.php @@ -27,9 +27,15 @@ public function process(ContainerBuilder $container) { 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 5915d4bed596..a5dc9551e374 100644 --- a/core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/PathSubscriber.php @@ -9,6 +9,7 @@ use Drupal\Core\CacheDecorator\AliasManagerCacheDecorator; use Drupal\Core\PathProcessor\InboundPathProcessorInterface; +use Drupal\Core\Routing\PathBasedGeneratorInterface; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\Event\GetResponseEvent; @@ -20,7 +21,18 @@ */ class PathSubscriber extends PathListenerBase implements EventSubscriberInterface { + /** + * The alias manager that caches alias lookups based on the request. + * + * @var \Drupal\Core\CacheDecorator\AliasManagerCacheDecorator + */ protected $aliasManager; + + /** + * A path processor manager for resolving the system path. + * + * @var \Drupal\Core\PathProcessor\InboundPathProcessorInterface + */ protected $pathProcessor; public function __construct(AliasManagerCacheDecorator $alias_manager, InboundPathProcessorInterface $path_processor) { @@ -39,6 +51,14 @@ public function onKernelRequestConvertPath(GetResponseEvent $event) { $path = trim($request->getPathInfo(), '/'); $path = $this->pathProcessor->processInbound($path, $request); $request->attributes->set('system_path', $path); + // Also set an attribute that indicates whether we are using clean URLs. + $clean_urls = TRUE; + $base_url = $request->getBaseUrl(); + if (!empty($base_url) && strpos($base_url, $request->getScriptName()) !== FALSE) { + $clean_urls = FALSE; + } + $request->attributes->set('clean_urls', $clean_urls); + // Set the cache key on the alias manager cache decorator. if ($event->getRequestType() == HttpKernelInterface::MASTER_REQUEST) { $this->aliasManager->setCacheKey($path); } diff --git a/core/lib/Drupal/Core/Language/LanguageManager.php b/core/lib/Drupal/Core/Language/LanguageManager.php index 673e8e307dca..3dea52a2f1af 100644 --- a/core/lib/Drupal/Core/Language/LanguageManager.php +++ b/core/lib/Drupal/Core/Language/LanguageManager.php @@ -134,7 +134,7 @@ public function reset($type = NULL) { * @return bool * TRUE if more than one language is enabled, FALSE otherwise. */ - protected function isMultilingual() { + public function isMultilingual() { return variable_get('language_count', 1) > 1; } diff --git a/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php b/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php new file mode 100644 index 000000000000..347a8777574b --- /dev/null +++ b/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php @@ -0,0 +1,35 @@ +<?php + +/** + * @file + * Contains Drupal\Core\PathProcessor\OutboundPathProcessorInterface. + */ + +namespace Drupal\Core\PathProcessor; + +use Symfony\Component\HttpFoundation\Request; + +/** + * Defines an interface for classes that process the outbound path. + */ +interface OutboundPathProcessorInterface { + + /** + * Processes the outbound path. + * + * @param string $path + * The path to process. + * + * @param array $options + * An array of options such as would be passed to the generator's + * generateFromPath() method. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The HttpRequest object representing the current request. + * + * @return + * The processed path. + */ + public function processOutbound($path, &$options = array(), Request $request = NULL); + +} diff --git a/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php b/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php index 7020710b7cbd..c663cf19b511 100644 --- a/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php +++ b/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php @@ -13,7 +13,7 @@ /** * Processes the inbound path using path alias lookups. */ -class PathProcessorAlias implements InboundPathProcessorInterface { +class PathProcessorAlias implements InboundPathProcessorInterface, OutboundPathProcessorInterface { /** * An alias manager for looking up the system path. @@ -40,4 +40,12 @@ public function processInbound($path, Request $request) { return $path; } + /** + * Implements Drupal\Core\PathProcessor\OutboundPathProcessorInterface::processOutbound(). + */ + public function processOutbound($path, &$options = array(), Request $request = NULL) { + $langcode = isset($options['language']) ? $options['language']->langcode : NULL; + $path = $this->aliasManager->getPathAlias($path, $langcode); + return $path; + } } diff --git a/core/lib/Drupal/Core/PathProcessor/PathProcessorFront.php b/core/lib/Drupal/Core/PathProcessor/PathProcessorFront.php index 872885ad6001..d03d19bf19a1 100644 --- a/core/lib/Drupal/Core/PathProcessor/PathProcessorFront.php +++ b/core/lib/Drupal/Core/PathProcessor/PathProcessorFront.php @@ -13,7 +13,7 @@ /** * Processes the inbound path by resolving it to the front page if empty. */ -class PathProcessorFront implements InboundPathProcessorInterface { +class PathProcessorFront implements InboundPathProcessorInterface, OutboundPathProcessorInterface { /** * A config factory for retrieving required config settings. @@ -45,4 +45,15 @@ public function processInbound($path, Request $request) { return $path; } + /** + * Implements Drupal\Core\PathProcessor\OutboundPathProcessorInterface::processOutbound(). + */ + public function processOutbound($path, &$options = array(), Request $request = NULL) { + // The special path '<front>' links to the default front page. + if ($path == '<front>') { + $path = ''; + } + return $path; + } + } diff --git a/core/lib/Drupal/Core/PathProcessor/PathProcessorManager.php b/core/lib/Drupal/Core/PathProcessor/PathProcessorManager.php index 1d0adff9e342..db8b79913483 100644 --- a/core/lib/Drupal/Core/PathProcessor/PathProcessorManager.php +++ b/core/lib/Drupal/Core/PathProcessor/PathProcessorManager.php @@ -7,7 +7,6 @@ namespace Drupal\Core\PathProcessor; -use Drupal\Core\PathProcessor\InboundPathProcessorInterface; use Symfony\Component\HttpFoundation\Request; /** @@ -16,10 +15,10 @@ * Holds an array of path processor objects and uses them to sequentially process * a path, in order of processor priority. */ -class PathProcessorManager implements InboundPathProcessorInterface { +class PathProcessorManager implements InboundPathProcessorInterface, OutboundPathProcessorInterface { /** - * Holds the array of processors to cycle through. + * Holds the array of inbound processors to cycle through. * * @var array * An array whose keys are priorities and whose values are arrays of path @@ -28,13 +27,31 @@ class PathProcessorManager implements InboundPathProcessorInterface { protected $inboundProcessors = array(); /** - * Holds the array of processors, sorted by priority. + * Holds the array of inbound processors, sorted by priority. * * @var array * An array of path processor objects. */ protected $sortedInbound = array(); + + /** + * Holds the array of outbound processors to cycle through. + * + * @var array + * An array whose keys are priorities and whose values are arrays of path + * processor objects. + */ + protected $outboundProcessors = array(); + + /** + * Holds the array of outbound processors, sorted by priority. + * + * @var array + * An array of path processor objects. + */ + protected $sortedOutbound = array(); + /** * Adds an inbound processor object to the $inboundProcessors property. * @@ -74,6 +91,46 @@ protected function getInbound() { 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(), Request $request = NULL) { + $processors = $this->getOutbound(); + foreach ($processors as $processor) { + $path = $processor->processOutbound($path, $options, $request); + } + 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. * diff --git a/core/lib/Drupal/Core/Routing/GeneratorNotInitializedException.php b/core/lib/Drupal/Core/Routing/GeneratorNotInitializedException.php new file mode 100644 index 000000000000..ab2922c677a2 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/GeneratorNotInitializedException.php @@ -0,0 +1,14 @@ +<?php +/** + * @file + * Definition of Drupal\Core\Routing\GeneratorNotInitializedException. + */ + +namespace Drupal\Core\Routing; + +use Exception; + +/** + * Class for exceptions thrown when the generator has not been initialized. + */ +class GeneratorNotInitializedException extends Exception { } diff --git a/core/lib/Drupal/Core/Routing/NullGenerator.php b/core/lib/Drupal/Core/Routing/NullGenerator.php index 7228514b9047..8970247b9771 100644 --- a/core/lib/Drupal/Core/Routing/NullGenerator.php +++ b/core/lib/Drupal/Core/Routing/NullGenerator.php @@ -6,6 +6,7 @@ */ namespace Drupal\Core\Routing; + use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\Exception\RouteNotFoundException; @@ -13,17 +14,23 @@ /** * No-op implementation of a Url Generator, needed for backward compatibility. */ -class NullGenerator implements UrlGeneratorInterface { +class NullGenerator extends UrlGenerator { + + /** + * Override the parent constructor. + */ + public function __construct() { + } /** - * Implements Symfony\Component\Routing\Generator\UrlGeneratorInterface::generate(); + * Overrides Drupal\Core\Routing\UrlGenerator::generate(); */ public function generate($name, $parameters = array(), $absolute = FALSE) { throw new RouteNotFoundException(); } /** - * Implements Symfony\Component\Routing\RequestContextAwareInterface::setContext(); + * Overrides Drupal\Core\Routing\UrlGenerator::setContext(); */ public function setContext(RequestContext $context) { } @@ -33,4 +40,11 @@ public function setContext(RequestContext $context) { */ public function getContext() { } + + /** + * Overrides Drupal\Core\Routing\UrlGenerator::processPath(). + */ + protected function processPath($path, &$options = array()) { + return $path; + } } diff --git a/core/lib/Drupal/Core/Routing/PathBasedGeneratorInterface.php b/core/lib/Drupal/Core/Routing/PathBasedGeneratorInterface.php new file mode 100644 index 000000000000..5c91e6251cd0 --- /dev/null +++ b/core/lib/Drupal/Core/Routing/PathBasedGeneratorInterface.php @@ -0,0 +1,93 @@ +<?php + +/** + * @file + * Contains Drupal\Core\Routing\PathBasedGeneratorInterface. + */ + +namespace Drupal\Core\Routing; + +use Symfony\Component\HttpFoundation\Request; + +/** + * Defines an interface for generating a url from a path as opposed to a route. + */ +interface PathBasedGeneratorInterface { + + /** + * Generates an internal or external URL. + * + * @param $path + * (optional) The internal path or external URL being linked to, such as + * "node/34" or "http://example.com/foo". + * + * @param $options + * (optional) An associative array of additional options. + * + * @return + * A string containing a URL to the given path. + */ + public function generateFromPath($path = NULL, $options = array()); + + /** + * Sets the $request property. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The HttpRequest object representing the current request. + */ + public function setRequest(Request $request); + + /** + * Sets the baseUrl property. + * + * This property is made up of scheme, host and base_path, e.g. + * 'http://www.example.com/mydrupalinstall/' + * + * The base url is usually set by the request but we allow it to be set + * independent of the request so that code that calls url() outside the context + * of a request can use the global $base_url variable to set this value. + * + * @todo Remove this once the url() function no longer supports being called + * when there is no request. + * + * @var string $url + * The base url to use for url generation. + */ + public function setBaseUrl($url); + + /** + * Sets the basePath property. + * + * This will be either '/' or '[subdir]/', where [subdir] is the name of the + * subdirectory that Drupal is running in. + * + * The base path is usually set by the request but we allow it to be set + * independent of the request so that code that calls url() outside the context + * of a request can use the global $base_url variable to set this value. + * + * @todo Remove this once the url() function no longer supports being called + * when there is no request. + * + * @var string $path + * The base path to use for url generation. + */ + public function setBasePath($path); + + /** + * Sets the scriptPath property. + * + * The script path is usually set by the request and is either 'index.php' or + * the empty string, depending on whether the request path actually contains + * the script path or not. We allow it to be set independent of the request so + * that code that calls url() outside the context of a request can use the global + * $script_path variable to set this value. + * + * @todo Remove this once the url() function no longer supports being called + * when there is no request. + * + * @var string $path + * The script path to use for url generation. + */ + public function setScriptPath($path); + +} diff --git a/core/lib/Drupal/Core/Routing/UrlGenerator.php b/core/lib/Drupal/Core/Routing/UrlGenerator.php index 75779a213cda..e6466fb5b926 100644 --- a/core/lib/Drupal/Core/Routing/UrlGenerator.php +++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php @@ -7,24 +7,66 @@ namespace Drupal\Core\Routing; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Log\LoggerInterface; +use Symfony\Component\Routing\Route as SymfonyRoute; +use Symfony\Component\Routing\Exception\RouteNotFoundException; + use Symfony\Cmf\Component\Routing\ProviderBasedGenerator; use Symfony\Cmf\Component\Routing\RouteProviderInterface; -use Drupal\Core\Path\AliasManagerInterface; +use Drupal\Component\Utility\Settings; +use Drupal\Component\Utility\UrlValidator; +use Drupal\Core\Config\ConfigFactory; +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 { + + /** + * A request object. + * + * @var \Symfony\Component\HttpFoundation\Request + */ + protected $request; + + /** + * The path processor to convert the system path to one suitable for urls. + * + * @var \Drupal\Core\PathProcessor\OutboundPathProcessorInterface + */ + protected $pathProcessor; + + /** + * The base path to use for urls. + * + * @var string + */ + protected $basePath; + + /** + * The base url to use for urls. + * + * @var string + */ + protected $baseUrl; + + /** + * The script path to use for urls. + * + * @var string + */ + protected $scriptPath; /** - * The alias manager that will be used to alias generated URLs. + * Whether both secure and insecure session cookies can be used simultaneously. * - * @var AliasManagerInterface + * @var bool */ - protected $aliasManager; + protected $mixedModeSessions; /** * Constructs a new generator object. @@ -36,23 +78,324 @@ 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, ConfigFactory $config, Settings $settings, LoggerInterface $logger = NULL) { parent::__construct($provider, $logger); - $this->aliasManager = $alias_manager; + $this->pathProcessor = $path_processor; + $this->mixedModeSessions = $settings->get('mixed_mode_sessions', FALSE); + $allowed_protocols = $config->get('system.filter')->get('protocols') ?: array('http', 'https'); + UrlValidator::setAllowedProtocols($allowed_protocols); + } + + /** + * Implements \Drupal\Core\Routing\PathBasedGeneratorInterface::setRequest(). + */ + public function setRequest(Request $request) { + $this->request = $request; + // Set some properties, based on the request, that are used during path-based + // url generation. + $this->basePath = $request->getBasePath() . '/'; + $this->baseUrl = $request->getSchemeAndHttpHost() . $this->basePath; + $this->scriptPath = ''; + $base_path_with_script = $request->getBaseUrl(); + $script_name = $request->getScriptName(); + if (!empty($base_path_with_script) && strpos($base_path_with_script, $script_name) !== FALSE) { + $length = strlen($this->basePath); + $this->scriptPath = ltrim(substr($script_name, $length), '/') . '/'; + } } /** - * 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); - // This method is expected to return a path with a leading /, whereas - // the alias manager has no leading /. - $path = '/' . $this->aliasManager->getPathAlias(trim($path, '/')); + if ($name instanceof SymfonyRoute) { + $route = $name; + } + elseif (null === $route = $this->provider->getRouteByName($name, $parameters)) { + throw new RouteNotFoundException(sprintf('Route "%s" does not exist.', $name)); + } + + // 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'])) { + $scheme_req = $route_requirements['_scheme']; + unset($route_requirements['_scheme']); + } + $path = $this->doGenerate($compiledRoute->getVariables(), $route->getDefaults(), $route_requirements, $compiledRoute->getTokens(), $parameters, $name, 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)); + } + + $path = $this->processPath($path); + if (!$absolute || !$host = $this->context->getHost()) { + return $base_url . $path; + } + + // Prepare an absolute URL by getting the correct scheme, host and port from + // the request context. + $scheme = $this->context->getScheme(); + if (isset($scheme_req) && ($req = strtolower($scheme_req)) && $scheme !== $req) { + $scheme = $req; + } + $port = ''; + if ('http' === $scheme && 80 != $this->context->getHttpPort()) { + $port = ':' . $this->context->getHttpPort(); + } elseif ('https' === $scheme && 443 != $this->context->getHttpsPort()) { + $port = ':' . $this->context->getHttpsPort(); + } + return $scheme . '://' . $host . $port . $base_url . $path; + } + + /** + * Implements \Drupal\Core\Routing\PathBasedGeneratorInterface::generateFromPath(). + * + * @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 '<front>'. 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 '<front>' 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 method + * 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. + * + * @throws \Drupal\Core\Routing\GeneratorNotInitializedException. + */ + public function generateFromPath($path = NULL, $options = array()) { + + if (!$this->initialized()) { + throw new GeneratorNotInitializedException(); + } + + // Merge in defaults. + $options += array( + 'fragment' => '', + 'query' => array(), + 'absolute' => FALSE, + 'prefix' => '', + ); + + 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)) && UrlValidator::stripDangerousProtocols($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 ? '&' : '?') . $this->httpBuildQuery($options['query']); + } + if (isset($options['https']) && $this->mixedModeSessions) { + 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), '/'); + } + + if (!isset($options['script'])) { + $options['script'] = $this->scriptPath; + } + // The base_url might be rewritten from the language rewrite in domain mode. + if (!isset($options['base_url'])) { + if (isset($options['https']) && $this->mixedModeSessions) { + if ($options['https'] === TRUE) { + $options['base_url'] = str_replace('http://', 'https://', $this->baseUrl); + $options['absolute'] = TRUE; + } + elseif ($options['https'] === FALSE) { + $options['base_url'] = str_replace('https://', 'http://', $this->baseUrl); + $options['absolute'] = TRUE; + } + } + else { + $options['base_url'] = $this->baseUrl; + } + } + elseif (rtrim($options['base_url'], '/') == $options['base_url']) { + $options['base_url'] .= '/'; + } + $base = $options['absolute'] ? $options['base_url'] : $this->basePath; + $prefix = empty($path) ? rtrim($options['prefix'], '/') : $options['prefix']; + + $path = str_replace('%2F', '/', rawurlencode($prefix . $path)); + $query = $options['query'] ? ('?' . $this->httpBuildQuery($options['query'])) : ''; + return $base . $options['script'] . $path . $query . $options['fragment']; + } + + /** + * Implements \Drupal\Core\Routing\PathBasedGeneratorInterface::setBaseUrl(). + */ + public function setBaseUrl($url) { + $this->baseUrl = $url; + } + + /** + * Implements \Drupal\Core\Routing\PathBasedGeneratorInterface::setBasePath(). + */ + public function setBasePath($path) { + $this->basePath = $path; + } + + /** + * Implements \Drupal\Core\Routing\PathBasedGeneratorInterface::setScriptPath(). + */ + public function setScriptPath($path) { + $this->scriptPath = $path; + } + + /** + * Parses an array into a valid, rawurlencoded query string. + * + * This differs from http_build_query() as we need to rawurlencode() (instead of + * urlencode()) all query parameters. + * + * @param $query + * The query parameter array to be processed, e.g. $_GET. + * @param $parent + * Internal use only. Used to build the $query array key for nested items. + * + * @return + * A rawurlencoded string which can be used as or appended to the URL query + * string. + * + * @see drupal_get_query_parameters() + * @ingroup php_wrappers + */ + public function httpBuildQuery(array $query, $parent = '') { + $params = array(); + + foreach ($query as $key => $value) { + $key = ($parent ? $parent . '[' . rawurlencode($key) . ']' : rawurlencode($key)); + + // Recurse into children. + if (is_array($value)) { + $params[] = $this->httpBuildQuery($value, $key); + } + // If a query parameter value is NULL, only append its key. + elseif (!isset($value)) { + $params[] = $key; + } + else { + // For better readability of paths in query strings, we decode slashes. + $params[] = $key . '=' . str_replace('%2F', '/', rawurlencode($value)); + } + } + return implode('&', $params); + } + + /** + * Passes the path to a processor manager to allow alterations. + */ + protected function processPath($path, &$options = array()) { + // Router-based paths may have a querystring on them. + if ($query_pos = strpos($path, '?')) { + // We don't need to do a strict check here because position 0 would mean we + // have no actual path to work with. + $actual_path = substr($path, 0, $query_pos); + $query_string = substr($path, $query_pos); + } + else { + $actual_path = $path; + $query_string = ''; + } + $path = '/' . $this->pathProcessor->processOutbound(trim($actual_path, '/'), $options, $this->request); + $path .= $query_string; return $path; } + /** + * Returns whether or not the url generator has been initialized. + * + * @return bool + * Returns TRUE if the basePath, baseUrl and scriptPath properties have been + * set, FALSE otherwise. + */ + protected function initialized() { + return isset($this->basePath) && isset($this->baseUrl) && isset($this->scriptPath); + } + } diff --git a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualUnitTest.php b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualUnitTest.php index 51e9a515b1e7..d2018ac503b2 100644 --- a/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualUnitTest.php +++ b/core/modules/contextual/lib/Drupal/contextual/Tests/ContextualUnitTest.php @@ -7,12 +7,12 @@ namespace Drupal\contextual\Tests; -use Drupal\simpletest\UnitTestBase; +use Drupal\simpletest\DrupalUnitTestBase; /** * Tests _contextual_links_to_id() & _contextual_id_to_links(). */ -class ContextualUnitTest extends UnitTestBase { +class ContextualUnitTest extends DrupalUnitTestBase { public static function getInfo() { return array( 'name' => 'Conversion to and from "contextual id"s (for placeholders)', diff --git a/core/modules/file/lib/Drupal/file/Tests/DownloadTest.php b/core/modules/file/lib/Drupal/file/Tests/DownloadTest.php index 0763507cc4fc..2c1278f5e4dd 100644 --- a/core/modules/file/lib/Drupal/file/Tests/DownloadTest.php +++ b/core/modules/file/lib/Drupal/file/Tests/DownloadTest.php @@ -7,6 +7,8 @@ namespace Drupal\file\Tests; +use Symfony\Component\HttpFoundation\Request; + /** * Tests for download/file transfer functions. */ @@ -84,7 +86,6 @@ function testPrivateFileTransfer() { * Test file_create_url(). */ function testFileCreateUrl() { - global $base_url, $script_path; // Tilde (~) is excluded from this test because it is encoded by // rawurlencode() in PHP 5.2 but not in PHP 5.3, as per RFC 3986. @@ -100,12 +101,18 @@ function testFileCreateUrl() { // generated by url(), whereas private files should be served by Drupal, so // their URLs should be generated by url(). The difference is most apparent // when $script_path is not empty (i.e., when not using clean URLs). - $script_path_original = $script_path; - foreach (array('', 'index.php/') as $script_path) { - $this->checkUrl('public', '', $basename, $base_url . '/' . file_stream_wrapper_get_instance_by_scheme('public')->getDirectoryPath() . '/' . $basename_encoded); - $this->checkUrl('private', '', $basename, $base_url . '/' . $script_path . 'system/files/' . $basename_encoded); + $clean_url_settings = array( + 'clean' => '', + 'unclean' => 'index.php/', + ); + $generator = $this->container->get('url_generator'); + foreach ($clean_url_settings as $clean_url_setting => $script_path) { + $clean_urls = $clean_url_setting == 'clean'; + $request = $this->prepareRequestForGenerator($clean_urls); + $base_path = $request->getSchemeAndHttpHost() . $request->getBasePath(); + $this->checkUrl('public', '', $basename, $base_path . '/' . file_stream_wrapper_get_instance_by_scheme('public')->getDirectoryPath() . '/' . $basename_encoded); + $this->checkUrl('private', '', $basename, $base_path . '/' . $script_path . 'system/files/' . $basename_encoded); } - $script_path = $script_path_original; } /** diff --git a/core/modules/forum/lib/Drupal/forum/Tests/ForumTest.php b/core/modules/forum/lib/Drupal/forum/Tests/ForumTest.php index 9f4b89a37c5e..f8c0479de700 100644 --- a/core/modules/forum/lib/Drupal/forum/Tests/ForumTest.php +++ b/core/modules/forum/lib/Drupal/forum/Tests/ForumTest.php @@ -467,7 +467,7 @@ function testForumWithNewPost() { // Login as the first user. $this->drupalLogin($this->admin_user); // Create a forum container. - $this->container = $this->createForum('container'); + $this->forumContainer = $this->createForum('container'); // Create a forum. $this->forum = $this->createForum('forum'); // Create a topic. diff --git a/core/modules/image/image.module b/core/modules/image/image.module index 17bb491d0152..2304c7d1f6de 100644 --- a/core/modules/image/image.module +++ b/core/modules/image/image.module @@ -723,12 +723,14 @@ function image_style_flush($style) { * The name of the style to be used with this image. * @param $path * The path to the image. + * @param $clean_urls + * (optional) Whether clean URLs are in use. * @return * The absolute URL where a style image can be downloaded, suitable for use * in an <img> tag. Requesting the URL will cause the image to be created. * @see image_style_deliver() */ -function image_style_url($style_name, $path) { +function image_style_url($style_name, $path, $clean_urls = NULL) { $uri = image_style_path($style_name, $path); // The token query is added even if the // 'image.settings:allow_insecure_derivatives' configuration is TRUE, so that @@ -743,11 +745,22 @@ function image_style_url($style_name, $path) { $token_query = array(IMAGE_DERIVATIVE_TOKEN => image_style_path_token($style_name, file_stream_wrapper_uri_normalize($path))); } + if ($clean_urls === NULL) { + // Assume clean URLs unless the request tells us otherwise. + $clean_urls = TRUE; + try { + $request = Drupal::request(); + $clean_urls = $request->attributes->get('clean_urls'); + } + catch (ServiceNotFoundException $e) { + } + } + // If not using clean URLs, the image derivative callback is only available // with the script path. If the file does not exist, use url() to ensure // that it is included. Once the file exists it's fine to fall back to the // actual file path, this avoids bootstrapping PHP once the files are built. - if ($GLOBALS['script_path'] && file_uri_scheme($uri) == 'public' && !file_exists($uri)) { + if ($clean_urls === FALSE && file_uri_scheme($uri) == 'public' && !file_exists($uri)) { $directory_path = file_stream_wrapper_get_instance_by_uri($uri)->getDirectoryPath(); return url($directory_path . '/' . file_uri_target($uri), array('absolute' => TRUE, 'query' => $token_query)); } diff --git a/core/modules/image/lib/Drupal/image/Tests/ImageStylesPathAndUrlTest.php b/core/modules/image/lib/Drupal/image/Tests/ImageStylesPathAndUrlTest.php index 52d3f4abba88..1b9e2dbb75de 100644 --- a/core/modules/image/lib/Drupal/image/Tests/ImageStylesPathAndUrlTest.php +++ b/core/modules/image/lib/Drupal/image/Tests/ImageStylesPathAndUrlTest.php @@ -8,6 +8,7 @@ namespace Drupal\image\Tests; use Drupal\simpletest\WebTestBase; +use Symfony\Component\HttpFoundation\Request; /** * Tests the functions for generating paths and URLs for image styles. @@ -104,8 +105,7 @@ function testImageStyleUrlForMissingSourceImage() { * Tests image_style_url(). */ function _testImageStyleUrlAndPath($scheme, $clean_url = TRUE, $extra_slash = FALSE) { - $script_path_original = $GLOBALS['script_path']; - $GLOBALS['script_path'] = $clean_url ? '' : 'index.php/'; + $request = $this->prepareRequestForGenerator($clean_url); // Make the default scheme neither "public" nor "private" to verify the // functions work for other than the default scheme. @@ -129,7 +129,7 @@ function _testImageStyleUrlAndPath($scheme, $clean_url = TRUE, $extra_slash = FA // Get the URL of a file that has not been generated and try to create it. $generated_uri = image_style_path($this->style_name, $original_uri); $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); - $generate_url = image_style_url($this->style_name, $original_uri); + $generate_url = image_style_url($this->style_name, $original_uri, $clean_url); // Ensure that the tests still pass when the file is generated by accessing // a poorly constructed (but still valid) file URL that has an extra slash @@ -137,11 +137,10 @@ function _testImageStyleUrlAndPath($scheme, $clean_url = TRUE, $extra_slash = FA if ($extra_slash) { $modified_uri = str_replace('://', ':///', $original_uri); $this->assertNotEqual($original_uri, $modified_uri, 'An extra slash was added to the generated file URI.'); - $generate_url = image_style_url($this->style_name, $modified_uri); + $generate_url = image_style_url($this->style_name, $modified_uri, $clean_url); } - - if ($GLOBALS['script_path']) { - $this->assertTrue(strpos($generate_url, $GLOBALS['script_path']) !== FALSE, 'When using non-clean URLS, the system path contains the script name.'); + if (!$clean_url) { + $this->assertTrue(strpos($generate_url, 'index.php/') !== FALSE, 'When using non-clean URLS, the system path contains the script name.'); } // Add some extra chars to the token. $this->drupalGet(str_replace(IMAGE_DERIVATIVE_TOKEN . '=', IMAGE_DERIVATIVE_TOKEN . '=Zo', $generate_url)); @@ -194,7 +193,7 @@ function _testImageStyleUrlAndPath($scheme, $clean_url = TRUE, $extra_slash = FA $this->assertNoRaw( chr(137) . chr(80) . chr(78) . chr(71) . chr(13) . chr(10) . chr(26) . chr(10), 'No PNG signature found in the response body.'); } } - elseif (!$GLOBALS['script_path']) { + elseif ($clean_url) { // Add some extra chars to the token. $this->drupalGet(str_replace(IMAGE_DERIVATIVE_TOKEN . '=', IMAGE_DERIVATIVE_TOKEN . '=Zo', $generate_url)); $this->assertResponse(200, 'Existing image was accessible at the URL wih an invalid token.'); @@ -219,11 +218,10 @@ function _testImageStyleUrlAndPath($scheme, $clean_url = TRUE, $extra_slash = FA config('image.settings')->set('suppress_itok_output', TRUE)->save(); $generated_uri = image_style_path($this->style_name, $original_uri); $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.'); - $generate_url = image_style_url($this->style_name, $original_uri); + $generate_url = image_style_url($this->style_name, $original_uri, $clean_url); $this->assertIdentical(strpos($generate_url, IMAGE_DERIVATIVE_TOKEN . '='), FALSE, 'The security token does not appear in the image style URL.'); $this->drupalGet($generate_url); $this->assertResponse(200, 'Image was accessible at the URL with a missing token.'); - $GLOBALS['script_path'] = $script_path_original; } } diff --git a/core/modules/language/language.module b/core/modules/language/language.module index a6ab9d284e7b..6b45fa1348b3 100644 --- a/core/modules/language/language.module +++ b/core/modules/language/language.module @@ -634,7 +634,6 @@ function language_language_negotiation_info() { 'callbacks' => array( 'negotiation' => 'language_from_url', 'language_switch' => 'language_switcher_url', - 'url_rewrite' => 'language_url_rewrite_url', ), 'file' => $file, 'weight' => -8, @@ -770,56 +769,6 @@ function language_preprocess_block(&$variables) { } } -/** - * Implements hook_url_outbound_alter(). - * - * Rewrite outbound URLs with language based prefixes. - */ -function language_url_outbound_alter(&$path, &$options, $original_path) { - // Only modify internal URLs. - if (!$options['external'] && language_multilingual()) { - static $drupal_static_fast; - if (!isset($drupal_static_fast)) { - $drupal_static_fast['callbacks'] = &drupal_static(__FUNCTION__); - } - $callbacks = &$drupal_static_fast['callbacks']; - - if (!isset($callbacks)) { - $callbacks = array(); - include_once DRUPAL_ROOT . '/core/includes/language.inc'; - - foreach (language_types_get_configurable() as $type) { - // Get URL rewriter callbacks only from enabled language methods. - $negotiation = variable_get("language_negotiation_$type", array()); - - foreach ($negotiation as $method_id => $method) { - if (isset($method['callbacks']['url_rewrite'])) { - if (isset($method['file'])) { - require_once DRUPAL_ROOT . '/' . $method['file']; - } - // Avoid duplicate callback entries. - $callbacks[$method['callbacks']['url_rewrite']] = TRUE; - } - } - } - - $callbacks = array_keys($callbacks); - } - - // No language dependent path allowed in this mode. - if (empty($callbacks)) { - unset($options['language']); - return; - } - - foreach ($callbacks as $callback) { - if (function_exists($callback)) { - $callback($path, $options); - } - } - } -} - /** * Returns language mappings between browser and Drupal language codes. * diff --git a/core/modules/language/language.negotiation.inc b/core/modules/language/language.negotiation.inc index d205d76aec34..82bf713c7500 100644 --- a/core/modules/language/language.negotiation.inc +++ b/core/modules/language/language.negotiation.inc @@ -424,83 +424,6 @@ function language_switcher_session($type, $path) { return $links; } -/** - * Rewrite URLs for the URL language negotiation method. - */ -function language_url_rewrite_url(&$path, &$options) { - static $drupal_static_fast; - if (!isset($drupal_static_fast)) { - $drupal_static_fast['languages'] = &drupal_static(__FUNCTION__); - } - $languages = &$drupal_static_fast['languages']; - - if (!isset($languages)) { - $languages = language_list(); - $languages = array_flip(array_keys($languages)); - } - - // Language can be passed as an option, or we go for current URL language. - if (!isset($options['language'])) { - $language_url = language(Language::TYPE_URL); - $options['language'] = $language_url; - } - // We allow only enabled languages here. - elseif (is_object($options['language']) && !isset($languages[$options['language']->langcode])) { - unset($options['language']); - 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])) { - // 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 = Drupal::request()->isSecure() ? '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']) && settings()->get('mixed_mode_sessions', 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; - - 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] . '/'; - } - break; - } - } -} - /** * Reads language prefixes and uses the langcode if no prefix is set. */ diff --git a/core/modules/language/language.services.yml b/core/modules/language/language.services.yml index 576f4f8c326d..d6599b330ac2 100644 --- a/core/modules/language/language.services.yml +++ b/core/modules/language/language.services.yml @@ -1,6 +1,7 @@ services: path_processor_language: class: Drupal\language\HttpKernel\PathProcessorLanguage + arguments: ['@config.factory', '@settings', '@language_manager'] tags: - { name: path_processor_inbound, priority: 300 } - arguments: ['@config.factory'] + - { name: path_processor_outbound, priority: 100 } diff --git a/core/modules/language/lib/Drupal/language/HttpKernel/PathProcessorLanguage.php b/core/modules/language/lib/Drupal/language/HttpKernel/PathProcessorLanguage.php index 006b31357d58..21bbbc80619b 100644 --- a/core/modules/language/lib/Drupal/language/HttpKernel/PathProcessorLanguage.php +++ b/core/modules/language/lib/Drupal/language/HttpKernel/PathProcessorLanguage.php @@ -7,15 +7,19 @@ namespace Drupal\language\HttpKernel; +use Drupal\Component\Utility\Settings; use Drupal\Core\Config\ConfigFactory; +use Drupal\Core\Language\Language; +use Drupal\Core\Language\LanguageManager; use Drupal\Core\PathProcessor\InboundPathProcessorInterface; +use Drupal\Core\PathProcessor\OutboundPathProcessorInterface; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpFoundation\Request; /** * Processes the inbound path using path alias lookups. */ -class PathProcessorLanguage implements InboundPathProcessorInterface { +class PathProcessorLanguage implements InboundPathProcessorInterface, OutboundPathProcessorInterface { /** * A config factory for retrieving required config settings. @@ -24,6 +28,20 @@ class PathProcessorLanguage implements InboundPathProcessorInterface { */ protected $config; + /** + * Whether both secure and insecure session cookies can be used simultaneously. + * + * @var bool + */ + protected $mixedModeSessions; + + /** + * Language manager for retrieving the url language type. + * + * @var \Drupal\Core\Language\LanguageManager + */ + protected $languageManager; + /** * An array of enabled languages. * @@ -31,6 +49,7 @@ class PathProcessorLanguage implements InboundPathProcessorInterface { */ protected $languages; + /** * Constructs a PathProcessorLanguage object. * @@ -41,8 +60,10 @@ class PathProcessorLanguage implements InboundPathProcessorInterface { * An array of languages, keyed by language code, representing the languages * currently enabled on the site. */ - public function __construct(ConfigFactory $config, array $languages = array()) { + public function __construct(ConfigFactory $config, Settings $settings, LanguageManager $language_manager, array $languages = array()) { $this->config = $config; + $this->mixedModeSessions = $settings->get('mixed_mode_sessions', FALSE); + $this->languageManager = $language_manager; if (empty($languages)) { $languages = language_list(); } @@ -69,4 +90,74 @@ public function processInbound($path, Request $request) { return $path; } + /** + * Implements Drupal\Core\PathProcessor\InboundPathProcessorInterface::processOutbound(). + */ + public function processOutbound($path, &$options = array(), Request $request = NULL) { + if (!$this->languageManager->isMultilingual()) { + return $path; + } + $url_scheme = 'http'; + $port = 80; + if ($request) { + $url_scheme = $request->getScheme(); + $port = $request->getPort(); + } + $languages = array_flip(array_keys($this->languages)); + // 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; + } + $url_source = $this->config->get('language.negotiation')->get('url.source'); + // @todo Go back to using a constant instead of the string 'path_prefix' once we can use a class + // constant. + if ($url_source == 'path_prefix') { + $prefixes = $this->config->get('language.negotiation')->get('url.prefixes'); + if (is_object($options['language']) && !empty($prefixes[$options['language']->langcode])) { + return empty($path) ? $prefixes[$options['language']->langcode] : $prefixes[$options['language']->langcode] . '/' . $path; + } + } + elseif ($url_source == 'domain') { + $domains = $this->config->get('language.negotiation')->get('url.domains'); + if (is_object($options['language']) && !empty($domains[$options['language']->langcode])) { + + // 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. + $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. + if (isset($normalized_base_url) && strpos($normalized_base_url, ':') !== FALSE) { + list($host, $port) = explode(':', $normalized_base_url); + $options['base_url'] .= ':' . $port; + } + elseif ($port != 80) { + $options['base_url'] .= ':' . $port; + } + + if (isset($options['https']) && $this->mixedModeSessions) { + 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']); + } + } + } + } + return $path; + } + } diff --git a/core/modules/language/lib/Drupal/language/Tests/LanguageUILanguageNegotiationTest.php b/core/modules/language/lib/Drupal/language/Tests/LanguageUILanguageNegotiationTest.php index 799d8d5e4283..8548f8eae664 100644 --- a/core/modules/language/lib/Drupal/language/Tests/LanguageUILanguageNegotiationTest.php +++ b/core/modules/language/lib/Drupal/language/Tests/LanguageUILanguageNegotiationTest.php @@ -452,6 +452,7 @@ function testLanguageDomain() { 'domain[it]' => 'it.example.com', ); $this->drupalPost('admin/config/regional/language/detection/url', $edit, t('Save configuration')); + $this->rebuildContainer(); // Build the link we're going to test. $link = 'it.example.com/admin'; @@ -466,17 +467,19 @@ function testLanguageDomain() { // Test HTTPS via options. $this->settingsSet('mixed_mode_sessions', TRUE); + $this->rebuildContainer(); + $italian_url = url('admin', array('https' => TRUE, 'language' => $languages['it'], 'script' => '')); $correct_link = 'https://' . $link; $this->assertTrue($italian_url == $correct_link, format_string('The url() function returns the right HTTPS URL (via options) (@url) in accordance with the chosen language', array('@url' => $italian_url))); $this->settingsSet('mixed_mode_sessions', FALSE); // Test HTTPS via current URL scheme. - $temp_https = $this->request->server->get('HTTPS'); - $this->request->server->set('HTTPS', 'on'); + $generator = $this->container->get('url_generator'); + $request = Request::create('', 'GET', array(), array(), array(), array('HTTPS' => 'on')); + $generator->setRequest($request); $italian_url = url('admin', array('language' => $languages['it'], 'script' => '')); $correct_link = 'https://' . $link; $this->assertTrue($italian_url == $correct_link, format_string('The url() function returns the right URL (via current URL scheme) (@url) in accordance with the chosen language', array('@url' => $italian_url))); - $this->request->server->set('HTTPS', $temp_https); } } diff --git a/core/modules/language/lib/Drupal/language/Tests/LanguageUrlRewritingTest.php b/core/modules/language/lib/Drupal/language/Tests/LanguageUrlRewritingTest.php index 3effed4fb804..07164841b3e2 100644 --- a/core/modules/language/lib/Drupal/language/Tests/LanguageUrlRewritingTest.php +++ b/core/modules/language/lib/Drupal/language/Tests/LanguageUrlRewritingTest.php @@ -8,6 +8,7 @@ namespace Drupal\language\Tests; use Drupal\simpletest\WebTestBase; +use Symfony\Component\HttpFoundation\Request; /** * Test that URL rewriting works as expected. @@ -47,8 +48,6 @@ function setUp() { // Reset static caching. drupal_static_reset('language_list'); - drupal_static_reset('language_url_outbound_alter'); - drupal_static_reset('language_url_rewrite_url'); } /** @@ -60,6 +59,7 @@ function testUrlRewritingEdgeCases() { $non_existing->langcode = $this->randomName(); $this->checkUrl($non_existing, 'Path language is ignored if language is not installed.', 'URL language negotiation does not work with non-installed languages'); + $request = $this->prepareRequestForGenerator(); // Check that URL rewriting is not applied to subrequests. $this->drupalGet('language_test/subrequest'); $this->assertText($this->web_user->name, 'Page correctly retrieved'); @@ -109,6 +109,9 @@ function testDomainNameNegotiationPort() { 'domain[fr]' => $language_domain ); $this->drupalPost('admin/config/regional/language/detection/url', $edit, t('Save configuration')); + // Rebuild the container so that the new language gets picked up by services + // that hold the list of languages. + $this->rebuildContainer(); // Enable domain configuration. config('language.negotiation') @@ -117,17 +120,12 @@ function testDomainNameNegotiationPort() { // Reset static caching. drupal_static_reset('language_list'); - drupal_static_reset('language_url_outbound_alter'); - drupal_static_reset('language_url_rewrite_url'); // In case index.php is part of the URLs, we need to adapt the asserted // URLs as well. $index_php = strpos(url('', array('absolute' => TRUE)), 'index.php') !== FALSE; - // Remember current HTTP_HOST. - $http_host = $_SERVER['HTTP_HOST']; - // Fake a different port. - $_SERVER['HTTP_HOST'] .= ':88'; + $request = $this->prepareRequestForGenerator(TRUE, array('SERVER_PORT' => '88')); // Create an absolute French link. $language = language_load('fr'); @@ -137,22 +135,18 @@ function testDomainNameNegotiationPort() { )); $expected = $index_php ? 'http://example.fr:88/index.php/' : 'http://example.fr:88/'; - $this->assertEqual($url, $expected, 'The right port is used.'); // If we set the port explicitly in url(), it should not be overriden. $url = url('', array( 'absolute' => TRUE, 'language' => $language, - 'base_url' => $GLOBALS['base_url'] . ':90', + 'base_url' => $request->getBaseUrl() . ':90', )); $expected = $index_php ? 'http://example.fr:90/index.php/' : 'http://example.fr:90/'; - $this->assertEqual($url, $expected, 'A given port is not overriden.'); - // Restore HTTP_HOST. - $_SERVER['HTTP_HOST'] = $http_host; } } diff --git a/core/modules/language/tests/language_test/language_test.module b/core/modules/language/tests/language_test/language_test.module index 150e024f5ccd..a77767de2c0f 100644 --- a/core/modules/language/tests/language_test/language_test.module +++ b/core/modules/language/tests/language_test/language_test.module @@ -119,5 +119,18 @@ function language_test_menu() { * Page callback. Uses a subrequest to retrieve the 'user' page. */ function language_test_subrequest() { - return drupal_container()->get('http_kernel')->handle(Request::create('/user'), HttpKernelInterface::SUB_REQUEST); + $request = Request::createFromGlobals(); + $server = $request->server->all(); + if (basename($server['SCRIPT_FILENAME']) != basename($server['SCRIPT_NAME'])) { + // We need this for when the test is executed by run-tests.sh. + // @todo Remove this once run-tests.sh has been converted to use a Request + // object. + $server['SCRIPT_FILENAME'] = $server['SCRIPT_NAME']; + $base_path = ltrim($server['REQUEST_URI'], '/'); + } + else { + $base_path = $request->getBasePath(); + } + $subrequest = Request::create($base_path . '/user', 'GET', $request->query->all(), $request->cookies->all(), array(), $server); + return Drupal::service('http_kernel')->handle($subrequest, HttpKernelInterface::SUB_REQUEST); } diff --git a/core/modules/locale/locale.install b/core/modules/locale/locale.install index 04452ac1b6dd..92b28ead9b1d 100644 --- a/core/modules/locale/locale.install +++ b/core/modules/locale/locale.install @@ -851,6 +851,10 @@ function locale_update_8011() { * Renames language_default language negotiation method to language_selected. */ function locale_update_8013() { + // @todo We only need language.inc here because LANGUAGE_NEGOTIATION_SELECTED + // is defined there. Remove this line once that has been converted to a class + // constant. + require_once DRUPAL_ROOT . '/core/includes/language.inc'; $weight = update_variable_get('language_negotiation_methods_weight_language_interface', NULL); if ($weight !== NULL) { $weight[LANGUAGE_NEGOTIATION_SELECTED] = $weight['language-default']; diff --git a/core/modules/path/lib/Drupal/path/Tests/PathLanguageTest.php b/core/modules/path/lib/Drupal/path/Tests/PathLanguageTest.php index f592b724b225..c3d13cf60022 100644 --- a/core/modules/path/lib/Drupal/path/Tests/PathLanguageTest.php +++ b/core/modules/path/lib/Drupal/path/Tests/PathLanguageTest.php @@ -97,10 +97,10 @@ function testAliasTranslation() { // Confirm that the alias is returned by url(). Languages are cached on // many levels, and we need to clear those caches. drupal_static_reset('language_list'); - drupal_static_reset('language_url_outbound_alter'); - drupal_static_reset('language_url_rewrite_url'); + $this->rebuildContainer(); $languages = language_list(); - $url = url('node/' . $french_node->nid, array('language' => $languages[$french_node->langcode])); + $url = $this->container->get('url_generator')->generateFromPath('node/' . $french_node->nid, array('language' => $languages[$french_node->langcode])); + $this->assertTrue(strpos($url, $edit['path[alias]']), 'URL contains the path alias.'); // Confirm that the alias works even when changing language negotiation diff --git a/core/modules/rdf/lib/Drupal/rdf/SiteSchema/BundleSchema.php b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/BundleSchema.php index 2c926969d32b..a20440a6bcc8 100644 --- a/core/modules/rdf/lib/Drupal/rdf/SiteSchema/BundleSchema.php +++ b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/BundleSchema.php @@ -49,7 +49,7 @@ public function __construct($site_schema, $entity_type, $bundle) { */ public function getUri() { $path = str_replace(array('{entity_type}', '{bundle}'), array($this->entityType, $this->bundle), static::$uriPattern); - return $this->siteSchema->getUri() . $path; + return $this->siteSchema->getUri() . '/' . $path; } /** diff --git a/core/modules/rdf/lib/Drupal/rdf/SiteSchema/EntitySchema.php b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/EntitySchema.php index 48a69fc0cc82..282c904dd53c 100644 --- a/core/modules/rdf/lib/Drupal/rdf/SiteSchema/EntitySchema.php +++ b/core/modules/rdf/lib/Drupal/rdf/SiteSchema/EntitySchema.php @@ -58,7 +58,7 @@ public function getGraph() { */ public function getUri() { $path = str_replace('{entity_type}', $this->entityType , static::$uriPattern); - return $this->siteSchema->getUri() . $path; + return $this->siteSchema->getUri() . '/' . $path; } /** diff --git a/core/modules/rdf/lib/Drupal/rdf/Tests/SiteSchemaTest.php b/core/modules/rdf/lib/Drupal/rdf/Tests/SiteSchemaTest.php index 0cfe07f87356..986118b658a3 100644 --- a/core/modules/rdf/lib/Drupal/rdf/Tests/SiteSchemaTest.php +++ b/core/modules/rdf/lib/Drupal/rdf/Tests/SiteSchemaTest.php @@ -47,7 +47,6 @@ function testSiteSchema() { 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type' => 'http://www.w3.org/2000/01/rdf-schema#class', 'http://www.w3.org/2000/01/rdf-schema#subClassOf' => url("$schema_path$entity_type", array('absolute' => TRUE)), ); - $this->assertEqual($bundle_schema->getUri(), $bundle_uri, 'Bundle term URI is generated correctly.'); $this->assertEqual($bundle_schema->getProperties(), $bundle_properties, 'Bundle term properties are generated correctly.'); } diff --git a/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php index b1102be3b626..2c80dcd8d502 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php @@ -174,6 +174,16 @@ public function containerBuild(ContainerBuilder $container) { ->setFactoryMethod('get') ->addArgument('state'); } + + if ($container->hasDefinition('path_processor_alias')) { + // Prevent the alias-based path processor, which requires a url_alias db + // table, from being registered to the path processor manager. We do this + // by removing the tags that the compiler pass looks for. This means the + // url generator can safely be used within DUTB tests. + $definition = $container->getDefinition('path_processor_alias'); + $definition->clearTag('path_processor_inbound')->clearTag('path_processor_outbound'); + } + } /** diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php index da3d89352f22..21cabdf5297b 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php @@ -19,6 +19,7 @@ use DOMXPath; use SimpleXMLElement; use Drupal\Core\Datetime\DrupalDateTime; +use Symfony\Component\HttpFoundation\Request; /** * Test case for typical Drupal tests. @@ -905,6 +906,16 @@ protected function writeSettings($settings) { } } + /** + * Overrides Drupal\simpletest\TestBase::rebuildContainer(). + */ + protected function rebuildContainer() { + parent::rebuildContainer(); + // Make sure the url generator has a request object, otherwise calls to + // $this->drupalGet() will fail. + $this->prepareRequestForGenerator(); + } + /** * Reset all data structures after having enabled new modules. * @@ -1181,7 +1192,7 @@ protected function parse() { * @param $path * Drupal path or URL to load into internal browser * @param $options - * Options to be forwarded to url(). + * Options to be forwarded to the url generator. * @param $headers * An array containing additional HTTP request headers, each formatted as * "name: value". @@ -1194,7 +1205,8 @@ protected function drupalGet($path, array $options = array(), array $headers = a // We re-using a CURL connection here. If that connection still has certain // options set, it might change the GET into a POST. Make sure we clear out // previous options. - $out = $this->curlExec(array(CURLOPT_HTTPGET => TRUE, CURLOPT_URL => url($path, $options), CURLOPT_NOBODY => FALSE, CURLOPT_HTTPHEADER => $headers)); + $url = $this->container->get('url_generator')->generateFromPath($path, $options); + $out = $this->curlExec(array(CURLOPT_HTTPGET => TRUE, CURLOPT_URL => $url, CURLOPT_NOBODY => FALSE, CURLOPT_HTTPHEADER => $headers)); $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up. // Replace original page output with new output from redirected page(s). @@ -1311,7 +1323,7 @@ protected function drupalGetAJAX($path, array $options = array(), array $headers * textfield: under these conditions, no button information is added to the * POST data. * @param $options - * Options to be forwarded to url(). + * Options to be forwarded to the url generator. * @param $headers * An array containing additional HTTP request headers, each formatted as * "name: value". @@ -1436,7 +1448,7 @@ protected function drupalPost($path, $edit, $submit, array $options = array(), a * element. In the absence of both the triggering element's Ajax path and * $ajax_path 'system/ajax' will be used. * @param $options - * (optional) Options to be forwarded to url(). + * (optional) Options to be forwarded to the url generator. * @param $headers * (optional) An array containing additional HTTP request headers, each * formatted as "name: value". Forwarded to drupalPost(). @@ -1644,7 +1656,7 @@ protected function checkForMetaRefresh() { * @param $path * Drupal path or URL to load into internal browser * @param $options - * Options to be forwarded to url(). + * Options to be forwarded to the url generator. * @param $headers * An array containing additional HTTP request headers, each formatted as * "name: value". @@ -1653,7 +1665,8 @@ protected function checkForMetaRefresh() { */ protected function drupalHead($path, array $options = array(), array $headers = array()) { $options['absolute'] = TRUE; - $out = $this->curlExec(array(CURLOPT_NOBODY => TRUE, CURLOPT_URL => url($path, $options), CURLOPT_HTTPHEADER => $headers)); + $url = $this->container->get('url_generator')->generateFromPath($path, $options); + $out = $this->curlExec(array(CURLOPT_NOBODY => TRUE, CURLOPT_URL => $url, CURLOPT_HTTPHEADER => $headers)); $this->refreshVariables(); // Ensure that any changes to variables in the other thread are picked up. if ($this->dumpHeaders) { @@ -2241,7 +2254,7 @@ protected function drupalSetSettings($settings) { * @param $path * The expected system path. * @param $options - * (optional) Any additional options to pass for $path to url(). + * (optional) Any additional options to pass for $path to the url generator. * @param $message * (optional) A message to display with the assertion. Do not translate * messages: use format_string() to embed variables in the message text, not @@ -2258,11 +2271,11 @@ protected function drupalSetSettings($settings) { protected function assertUrl($path, array $options = array(), $message = '', $group = 'Other') { if (!$message) { $message = t('Current URL is @url.', array( - '@url' => var_export(url($path, $options), TRUE), + '@url' => var_export($this->container->get('url_generator')->generateFromPath($path, $options), TRUE), )); } $options['absolute'] = TRUE; - return $this->assertEqual($this->getUrl(), url($path, $options), $message, $group); + return $this->assertEqual($this->getUrl(), $this->container->get('url_generator')->generateFromPath($path, $options), $message, $group); } /** @@ -3252,4 +3265,50 @@ protected function verboseEmail($count = 1) { $this->verbose(t('Email:') . '<pre>' . print_r($mail, TRUE) . '</pre>'); } } + + /** + * Creates a mock request and sets it on the generator. + * + * This is used to manipulate how the generator generates paths during tests. + * It also ensures that calls to $this->drupalGet() will work when running + * from run-tests.sh because the url generator no longer looks at the global + * variables that are set there but relies on getting this information from a + * request object. + * + * @param bool $clean_urls + * Whether to mock the request using clean urls. + * + * @param $override_server_vars + * An array of server variables to override. + * + * @return $request + * The mocked request object. + */ + protected function prepareRequestForGenerator($clean_urls = TRUE, $override_server_vars = array()) { + $generator = $this->container->get('url_generator'); + $request = Request::createFromGlobals(); + $server = $request->server->all(); + if (basename($server['SCRIPT_FILENAME']) != basename($server['SCRIPT_NAME'])) { + // We need this for when the test is executed by run-tests.sh. + // @todo Remove this once run-tests.sh has been converted to use a Request + // object. + $cwd = getcwd(); + $server['SCRIPT_FILENAME'] = $cwd . '/' . basename($server['SCRIPT_NAME']); + $base_path = rtrim($server['REQUEST_URI'], '/'); + } + else { + $base_path = $request->getBasePath(); + } + if ($clean_urls) { + $request_path = $base_path ? $base_path . '/user' : 'user'; + } + else { + $request_path = $base_path ? $base_path . '/index.php/user' : '/index.php/user'; + } + $server = array_merge($server, $override_server_vars); + + $request = Request::create($request_path, 'GET', array(), array(), array(), $server); + $generator->setRequest($request); + return $request; + } } diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/UrlTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/UrlTest.php index a502e82823a5..c3b46a25722c 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Common/UrlTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Common/UrlTest.php @@ -8,6 +8,7 @@ namespace Drupal\system\Tests\Common; use Drupal\simpletest\WebTestBase; +use Symfony\Component\HttpFoundation\Request; /** * Tests for URL generation functions. @@ -263,50 +264,6 @@ function testDrupalParseUrl() { $this->assertFalse(valid_url($parts['path'], TRUE), 'drupal_parse_url() correctly parsed a forged URL.'); } - /** - * Tests url() functionality. - * - * Tests url() with/without query, with/without fragment, absolute on/off and - * asserts all that works when clean URLs are on and off. - */ - function testUrl() { - global $base_url, $script_path; - - $script_path_original = $script_path; - foreach (array('', 'index.php/') as $script_path) { - foreach (array(FALSE, TRUE) as $absolute) { - // Get the expected start of the path string. - $base = ($absolute ? $base_url . '/' : base_path()) . $script_path; - $absolute_string = $absolute ? 'absolute' : NULL; - - $url = $base . 'node/123'; - $result = url('node/123', array('absolute' => $absolute)); - $this->assertEqual($url, $result, "$url == $result"); - - $url = $base . 'node/123#foo'; - $result = url('node/123', array('fragment' => 'foo', 'absolute' => $absolute)); - $this->assertEqual($url, $result, "$url == $result"); - - $url = $base . 'node/123?foo'; - $result = url('node/123', array('query' => array('foo' => NULL), 'absolute' => $absolute)); - $this->assertEqual($url, $result, "$url == $result"); - - $url = $base . 'node/123?foo=bar&bar=baz'; - $result = url('node/123', array('query' => array('foo' => 'bar', 'bar' => 'baz'), 'absolute' => $absolute)); - $this->assertEqual($url, $result, "$url == $result"); - - $url = $base . 'node/123?foo#bar'; - $result = url('node/123', array('query' => array('foo' => NULL), 'fragment' => 'bar', 'absolute' => $absolute)); - $this->assertEqual($url, $result, "$url == $result"); - - $url = $base; - $result = url('<front>', array('absolute' => $absolute)); - $this->assertEqual($url, $result, "$url == $result"); - } - } - $script_path = $script_path_original; - } - /** * Tests external URL handling. */ diff --git a/core/modules/system/lib/Drupal/system/Tests/Path/UrlAlterFunctionalTest.php b/core/modules/system/lib/Drupal/system/Tests/Path/UrlAlterFunctionalTest.php index 6b53b2246c30..eef40d4e7349 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Path/UrlAlterFunctionalTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Path/UrlAlterFunctionalTest.php @@ -46,7 +46,8 @@ function testUrlAlter() { // Test that a path always uses its alias. $path = array('source' => "user/$uid/test1", 'alias' => 'alias/test1'); - drupal_container()->get('path.crud')->save($path['source'], $path['alias']); + $this->container->get('path.crud')->save($path['source'], $path['alias']); + $this->rebuildContainer(); $this->assertUrlInboundAlter('alias/test1', "user/$uid/test1"); $this->assertUrlOutboundAlter("user/$uid/test1", 'alias/test1'); @@ -100,7 +101,7 @@ function testCurrentUrlRequestedPath() { */ protected function assertUrlOutboundAlter($original, $final) { // Test outbound altering. - $result = url($original); + $result = $this->container->get('url_generator')->generateFromPath($original); $base_path = base_path() . $GLOBALS['script_path']; $result = substr($result, strlen($base_path)); $this->assertIdentical($result, $final, format_string('Altered outbound URL %original, expected %final, and got %result.', array('%original' => $original, '%final' => $final, '%result' => $result))); diff --git a/core/modules/system/lib/Drupal/system/Tests/Routing/UrlGeneratorTest.php b/core/modules/system/lib/Drupal/system/Tests/Routing/UrlGeneratorTest.php deleted file mode 100644 index 0e9f13e5f983..000000000000 --- a/core/modules/system/lib/Drupal/system/Tests/Routing/UrlGeneratorTest.php +++ /dev/null @@ -1,76 +0,0 @@ -<?php - -/** - * @file - * Contains Drupal\system\Tests\Routing\UrlGeneratorTest. - */ - -namespace Drupal\system\Tests\Routing; - -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Routing\Route; -use Symfony\Component\Routing\RouteCollection; -use Symfony\Component\Routing\RequestContext; - -use Drupal\simpletest\UnitTestBase; - -use Drupal\Core\Routing\UrlGenerator; - -/** - * Basic tests for the Route. - */ -class UrlGeneratorTest extends UnitTestBase { - - protected $generator; - - protected $aliasManager; - - public static function getInfo() { - return array( - 'name' => 'UrlGenerator', - 'description' => 'Confirm that the UrlGenerator is functioning properly.', - 'group' => 'Routing', - ); - } - - function setUp() { - parent::setUp(); - - $routes = new RouteCollection(); - $routes->add('test_1', new Route('/test/one')); - $routes->add('test_2', new Route('/test/two/{narf}')); - $provider = new MockRouteProvider($routes); - - $this->aliasManager = new MockAliasManager(); - $this->aliasManager->addAlias('test/one', 'hello/world'); - - $context = new RequestContext(); - $context->fromRequest(Request::create('/some/path')); - - $generator = new UrlGenerator($provider, $this->aliasManager); - $generator->setContext($context); - - $this->generator = $generator; - } - - /** - * Confirms that generated routes will have aliased paths. - */ - public function testAliasGeneration() { - $url = $this->generator->generate('test_1'); - - $this->assertEqual($url, '/hello/world', 'Correct URL generated including alias.'); - } - - /** - * Confirms that generated routes will have aliased paths. - */ - 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.'); - } - -} diff --git a/core/modules/system/lib/Drupal/system/Tests/Upgrade/LanguageUpgradePathTest.php b/core/modules/system/lib/Drupal/system/Tests/Upgrade/LanguageUpgradePathTest.php index 47718699b9d1..ba62946b1a66 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Upgrade/LanguageUpgradePathTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Upgrade/LanguageUpgradePathTest.php @@ -120,6 +120,11 @@ public function testLanguageUpgrade() { $this->assertTrue(isset($current_weights['language-selected']), 'Language-selected is present.'); $this->assertFalse(isset($current_weights['language-default']), 'Language-default is not present.'); + // @todo We only need language.inc here because LANGUAGE_NEGOTIATION_SELECTED + // is defined there. Remove this line once that has been converted to a class + // constant. + require_once DRUPAL_ROOT . '/core/includes/language.inc'; + // Check that negotiation callback was added to language_negotiation_language_interface. $language_negotiation_language_interface = update_variable_get('language_negotiation_language_interface', NULL); $this->assertTrue(isset($language_negotiation_language_interface[LANGUAGE_NEGOTIATION_SELECTED]['callbacks']['negotiation']), 'Negotiation callback was added to language_negotiation_language_interface.'); diff --git a/core/modules/system/lib/Drupal/system/Tests/Upgrade/UpgradePathTestBase.php b/core/modules/system/lib/Drupal/system/Tests/Upgrade/UpgradePathTestBase.php index 73edd7aa938c..b613121d8884 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Upgrade/UpgradePathTestBase.php +++ b/core/modules/system/lib/Drupal/system/Tests/Upgrade/UpgradePathTestBase.php @@ -9,6 +9,8 @@ use Drupal\Component\Utility\Crypt; use Drupal\Core\Database\Database; +use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\DrupalKernel; use Drupal\simpletest\WebTestBase; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\HttpFoundation\Request; @@ -18,6 +20,11 @@ */ abstract class UpgradePathTestBase extends WebTestBase { + /** + * @var array + */ + protected $configDirectories; + /** * The file path(s) to the dumped database(s) to load into the child site. * @@ -103,6 +110,12 @@ protected function setUp() { $conf = array(); drupal_static_reset(); + + // Build a minimal, partially mocked environment for unit tests. + $this->containerBuild(drupal_container()); + // Make sure it survives kernel rebuilds. + $conf['container_bundles'][] = 'Drupal\simpletest\TestBundle'; + // Change the database prefix. // All static variables need to be reset before the database prefix is // changed, since Drupal\Core\Utility\CacheArray implementations attempt to @@ -300,6 +313,27 @@ protected function performUpgrade($register_errors = TRUE) { return TRUE; } + /** + * Overrides some core services for the upgrade tests. + */ + public function containerBuild(ContainerBuilder $container) { + // Keep the container object around for tests. + $this->container = $container; + + $container + ->register('config.storage', 'Drupal\Core\Config\FileStorage') + ->addArgument($this->configDirectories[CONFIG_ACTIVE_DIRECTORY]); + + if ($this->container->hasDefinition('path_processor_alias')) { + // Prevent the alias-based path processor, which requires a url_alias db + // table, from being registered to the path processor manager. We do this + // by removing the tags that the compiler pass looks for. This means the + // url generator can safely be used within upgrade path tests. + $definition = $this->container->getDefinition('path_processor_alias'); + $definition->clearTag('path_processor_inbound')->clearTag('path_processor_outbound'); + } + } + /** * Gets update.php without calling url(). * diff --git a/core/modules/system/tests/https.php b/core/modules/system/tests/https.php index cc2bd45240cd..e509c157da31 100644 --- a/core/modules/system/tests/https.php +++ b/core/modules/system/tests/https.php @@ -3,6 +3,9 @@ /** * @file * Fake an HTTPS request, for use during testing. + * + * @todo Fix this to use a new request rather than modifying server variables, + * see http.php. */ // Set a global variable to indicate a mock HTTPS request. diff --git a/core/modules/system/tests/modules/url_alter_test/lib/Drupal/url_alter_test/PathProcessorTest.php b/core/modules/system/tests/modules/url_alter_test/lib/Drupal/url_alter_test/PathProcessorTest.php new file mode 100644 index 000000000000..e649f6d05c4d --- /dev/null +++ b/core/modules/system/tests/modules/url_alter_test/lib/Drupal/url_alter_test/PathProcessorTest.php @@ -0,0 +1,61 @@ +<?php + +/** + * @file + * Contains Drupal\url_alter_test\PathProcessorTest. + */ + +namespace Drupal\url_alter_test; + +use Drupal\Core\PathProcessor\InboundPathProcessorInterface; +use Drupal\Core\PathProcessor\OutboundPathProcessorInterface; +use Symfony\Component\HttpFoundation\Request; + +/** + * Path processor for url_alter_test. + */ +class PathProcessorTest implements InboundPathProcessorInterface, OutboundPathProcessorInterface { + + /** + * Implements Drupal\Core\PathProcessor\InboundPathProcessorInterface::processInbound(). + */ + public function processInbound($path, Request $request) { + // Rewrite user/username to user/uid. + if (preg_match('!^user/([^/]+)(/.*)?!', $path, $matches)) { + if ($account = user_load_by_name($matches[1])) { + $matches += array(2 => ''); + $path = 'user/' . $account->uid . $matches[2]; + } + } + + // Rewrite community/ to forum/. + if ($path == 'community' || strpos($path, 'community/') === 0) { + $path = 'forum' . substr($path, 9); + } + + if ($path == 'url-alter-test/bar') { + $path = 'url-alter-test/foo'; + } + return $path; + } + + /** + * Implements Drupal\Core\PathProcessor\OutboundPathProcessorInterface::processOutbound(). + */ + public function processOutbound($path, &$options = array(), Request $request = NULL) { + // Rewrite user/uid to user/username. + if (preg_match('!^user/([0-9]+)(/.*)?!', $path, $matches)) { + if ($account = user_load($matches[1])) { + $matches += array(2 => ''); + $path = 'user/' . $account->name . $matches[2]; + } + } + + // Rewrite forum/ to community/. + if ($path == 'forum' || strpos($path, 'forum/') === 0) { + $path = 'community' . substr($path, 5); + } + return $path; + } + +} diff --git a/core/modules/system/tests/modules/url_alter_test/url_alter_test.module b/core/modules/system/tests/modules/url_alter_test/url_alter_test.module index 8bacb9b1e71f..a7567a3bac0a 100644 --- a/core/modules/system/tests/modules/url_alter_test/url_alter_test.module +++ b/core/modules/system/tests/modules/url_alter_test/url_alter_test.module @@ -25,21 +25,3 @@ function url_alter_test_foo() { print 'current_path=' . current_path() . ' request_path=' . request_path(); exit; } - -/** - * Implements hook_url_outbound_alter(). - */ -function url_alter_test_url_outbound_alter(&$path, &$options, $original_path) { - // Rewrite user/uid to user/username. - if (preg_match('!^user/([0-9]+)(/.*)?!', $path, $matches)) { - if ($account = user_load($matches[1])) { - $matches += array(2 => ''); - $path = 'user/' . $account->name . $matches[2]; - } - } - - // Rewrite forum/ to community/. - if ($path == 'forum' || strpos($path, 'forum/') === 0) { - $path = 'community' . substr($path, 5); - } -} diff --git a/core/modules/system/tests/modules/url_alter_test/url_alter_test.services.yml b/core/modules/system/tests/modules/url_alter_test/url_alter_test.services.yml index a05b3636c1fa..58b98bada783 100644 --- a/core/modules/system/tests/modules/url_alter_test/url_alter_test.services.yml +++ b/core/modules/system/tests/modules/url_alter_test/url_alter_test.services.yml @@ -1,5 +1,6 @@ services: - url_alter_test.path_subscriber: - class: Drupal\url_alter_test\PathProcessor + url_alter_test.path_processor: + class: Drupal\url_alter_test\PathProcessorTest tags: - { name: path_processor_inbound, priority: 800 } + - { name: path_processor_outbound, priority: 100 } diff --git a/core/modules/translation/lib/Drupal/translation/Tests/TranslationTest.php b/core/modules/translation/lib/Drupal/translation/Tests/TranslationTest.php index 4fc436a2a569..ff55606a52cd 100644 --- a/core/modules/translation/lib/Drupal/translation/Tests/TranslationTest.php +++ b/core/modules/translation/lib/Drupal/translation/Tests/TranslationTest.php @@ -281,8 +281,7 @@ function testTranslateOwnContentRole() { */ function resetCaches() { drupal_static_reset('language_list'); - drupal_static_reset('language_url_outbound_alter'); - drupal_static_reset('language_url_rewrite_url'); + $this->rebuildContainer(); } /** diff --git a/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationTestBase.php b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationTestBase.php index 870321fc753c..31592c7b64de 100644 --- a/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationTestBase.php +++ b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationTestBase.php @@ -82,6 +82,10 @@ function setUp() { $this->setupTestFields(); $this->controller = translation_entity_controller($this->entityType); + + // Rebuild the container so that the new languages are picked up by services + // that hold a list of languages. + $this->rebuildContainer(); } /** diff --git a/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationWorkflowsTest.php b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationWorkflowsTest.php index de7db8529901..d28425b05d64 100644 --- a/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationWorkflowsTest.php +++ b/core/modules/translation_entity/lib/Drupal/translation_entity/Tests/EntityTranslationWorkflowsTest.php @@ -68,6 +68,7 @@ protected function setupEntity() { $this->drupalLogin($this->translator); $add_translation_path = $this->controller->getBasePath($this->entity) . "/translations/add/$default_langcode/{$this->langcodes[2]}"; $this->drupalPost($add_translation_path, array(), t('Save')); + $this->rebuildContainer(); } /** diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh index 62fca077f31c..49b1a4476e8e 100755 --- a/core/scripts/run-tests.sh +++ b/core/scripts/run-tests.sh @@ -285,8 +285,10 @@ function simpletest_script_init($server_software) { if (!empty($args['url'])) { $parsed_url = parse_url($args['url']); $host = $parsed_url['host'] . (isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''); - $path = isset($parsed_url['path']) ? $parsed_url['path'] : ''; - + $path = isset($parsed_url['path']) ? rtrim($parsed_url['path']) : ''; + if ($path == '/') { + $path = ''; + } // If the passed URL schema is 'https' then setup the $_SERVER variables // properly so that testing will run under HTTPS. if ($parsed_url['scheme'] == 'https') { diff --git a/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorTest.php b/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorTest.php index f150ace9cef5..fdbcee62e66f 100644 --- a/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorTest.php +++ b/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorTest.php @@ -7,6 +7,7 @@ namespace Drupal\Tests\Core\PathProcessor; +use Drupal\Component\Utility\Settings; use Drupal\Core\PathProcessor\PathProcessorAlias; use Drupal\Core\PathProcessor\PathProcessorDecode; use Drupal\Core\PathProcessor\PathProcessorFront; @@ -22,6 +23,7 @@ class PathProcessorTest extends UnitTestCase { protected $languages; + protected $languageManager; public static function getInfo() { return array( @@ -43,6 +45,13 @@ public function setUp() { } $this->languages = $languages; + // Create a language manager stub. + $language_manager = $this->getMock('Drupal\Core\Language\LanguageManager'); + $language_manager->expects($this->any()) + ->method('getLanguage') + ->will($this->returnValue($languages['en'])); + + $this->languageManager = $language_manager; } /** @@ -86,7 +95,7 @@ function testProcessInbound() { $alias_processor = new PathProcessorAlias($alias_manager); $decode_processor = new PathProcessorDecode(); $front_processor = new PathProcessorFront($config_factory_stub); - $language_processor = new PathProcessorLanguage($config_factory_stub, $this->languages); + $language_processor = new PathProcessorLanguage($config_factory_stub, new Settings(array()), $this->languageManager, $this->languages); // First, test the processor manager with the processors in the incorrect // order. The alias processor will run before the language processor, meaning diff --git a/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php b/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php new file mode 100644 index 000000000000..079213e6a9c2 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php @@ -0,0 +1,167 @@ +<?php + +/** + * @file + * Contains Drupal\Tests\Core\Routing\UrlGeneratorTest. + */ + +namespace Drupal\Tests\Core\Routing; + +use Drupal\Component\Utility\Settings; +use Drupal\Core\Config\ConfigFactory; +use Drupal\Core\Config\NullStorage; +use Drupal\Core\Config\Context\ConfigContextFactory; +use Drupal\Core\PathProcessor\PathProcessorAlias; +use Drupal\Core\PathProcessor\PathProcessorManager; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Routing\RequestContext; + +use Drupal\Tests\UnitTestCase; + +use Drupal\Core\Routing\UrlGenerator; + +/** + * Basic tests for the Route. + * + * @group Routing + */ +class UrlGeneratorTest extends UnitTestCase { + + protected $generator; + + protected $aliasManager; + + public static function getInfo() { + return array( + 'name' => 'UrlGenerator', + 'description' => 'Confirm that the UrlGenerator is functioning properly.', + 'group' => 'Routing', + ); + } + + function setUp() { + + $routes = new RouteCollection(); + $first_route = new Route('/test/one'); + $second_route = new Route('/test/two/{narf}'); + $routes->add('test_1', $first_route); + $routes->add('test_2', $second_route); + + // Create a route provider stub. + $provider = $this->getMockBuilder('Drupal\Core\Routing\RouteProvider') + ->disableOriginalConstructor() + ->getMock(); + $route_name_return_map = array( + array('test_1', array(), $first_route), + array('test_2', array('narf' => '5'), $second_route), + ); + $provider->expects($this->any()) + ->method('getRouteByName') + ->will($this->returnValueMap($route_name_return_map)); + $routes_names_return_map = array( + array(array('test_1'), array(), array($first_route)), + array(array('test_2'), array('narf' => '5'), array($second_route)), + ); + $provider->expects($this->any()) + ->method('getRoutesByNames') + ->will($this->returnValueMap($routes_names_return_map)); + + // Create an alias manager stub. + $alias_manager = $this->getMockBuilder('Drupal\Core\Path\AliasManager') + ->disableOriginalConstructor() + ->getMock(); + $alias_map = array( + array('test/one', NULL, 'hello/world'), + array('test/two/5', NULL, 'goodbye/cruel/world'), + array('node/123', NULL, 'node/123'), + ); + $alias_manager->expects($this->any()) + ->method('getPathAlias') + ->will($this->returnValueMap($alias_map)); + + $this->aliasManager = $alias_manager; + + $context = new RequestContext(); + $context->fromRequest(Request::create('/some/path')); + + $processor = new PathProcessorAlias($this->aliasManager); + $processor_manager = new PathProcessorManager(); + $processor_manager->addOutbound($processor, 1000); + + $config_factory_stub = $this->getConfigFactoryStub(array('system.filter' => array('protocols' => array('http', 'https')))); + + $generator = new UrlGenerator($provider, $processor_manager, $config_factory_stub, new Settings(array())); + $generator->setContext($context); + + $this->generator = $generator; + } + + /** + * Confirms that generated routes will have aliased paths. + */ + public function testAliasGeneration() { + $url = $this->generator->generate('test_1'); + $this->assertEquals('/hello/world', $url); + } + + /** + * Confirms that generated routes will have aliased paths. + */ + public function testAliasGenerationWithParameters() { + $url = $this->generator->generate('test_2', array('narf' => '5')); + $this->assertEquals('/goodbye/cruel/world', $url, 'Correct URL generated including alias and parameters.'); + } + + /** + * Confirms that absolute URLs work with generated routes. + */ + public function testAbsoluteURLGeneration() { + $url = $this->generator->generate('test_1', array(), TRUE); + $this->assertEquals('http://localhost/hello/world', $url); + } + + /** + * Tests path-based URL generation. + */ + public function testPathBasedURLGeneration() { + $base_path = '/subdir'; + $base_url = 'http://www.example.com' . $base_path; + $this->generator->setBasePath($base_path . '/'); + $this->generator->setBaseUrl($base_url . '/'); + foreach (array('', 'index.php/') as $script_path) { + $this->generator->setScriptPath($script_path); + foreach (array(FALSE, TRUE) as $absolute) { + // Get the expected start of the path string. + $base = ($absolute ? $base_url . '/' : $base_path . '/') . $script_path; + $absolute_string = $absolute ? 'absolute' : NULL; + $url = $base . 'node/123'; + $result = $this->generator->generateFromPath('node/123', array('absolute' => $absolute)); + $this->assertEquals($url, $result, "$url == $result"); + + $url = $base . 'node/123#foo'; + $result = $this->generator->generateFromPath('node/123', array('fragment' => 'foo', 'absolute' => $absolute)); + $this->assertEquals($url, $result, "$url == $result"); + + $url = $base . 'node/123?foo'; + $result = $this->generator->generateFromPath('node/123', array('query' => array('foo' => NULL), 'absolute' => $absolute)); + $this->assertEquals($url, $result, "$url == $result"); + + $url = $base . 'node/123?foo=bar&bar=baz'; + $result = $this->generator->generateFromPath('node/123', array('query' => array('foo' => 'bar', 'bar' => 'baz'), 'absolute' => $absolute)); + $this->assertEquals($url, $result, "$url == $result"); + + $url = $base . 'node/123?foo#bar'; + $result = $this->generator->generateFromPath('node/123', array('query' => array('foo' => NULL), 'fragment' => 'bar', 'absolute' => $absolute)); + $this->assertEquals($url, $result, "$url == $result"); + + $url = $base; + $result = $this->generator->generateFromPath('<front>', array('absolute' => $absolute)); + $this->assertEquals($url, $result, "$url == $result"); + } + } + } + +} -- GitLab