Commit b2194392 authored by alexpott's avatar alexpott

Issue #1888424 by katbailey, steveoliver, twistor, beejeebus, effulgentsia:...

Issue #1888424 by katbailey, steveoliver, twistor, beejeebus, effulgentsia: Change notice: Make Drupal's URL generation logic available to HttpKernel, and minimize code repetition/divergence.
parent 9d735990
......@@ -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
......
This diff is collapsed.
......@@ -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()));
}
}
......
......@@ -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.
......
......@@ -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');
}
}
......@@ -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));
}
}
}
......@@ -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);
}
......
......@@ -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;
}
......
<?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);
}
......@@ -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;
}
}
......@@ -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;
}
}
......@@ -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.
*
......
<?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 { }
......@@ -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;
}
}
<?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);
}
......@@ -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)',
......
......@@ -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;
}
/**
......
......@@ -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.
......
......@@ -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));
}
......
......@@ -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;
}
}
......@@ -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());