diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 990a360..0ef2d9d 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -3,6 +3,8 @@ use Symfony\Component\ClassLoader\UniversalClassLoader; use Symfony\Component\ClassLoader\ApcUniversalClassLoader; +use Drupal\Core\Request; + /** * @file * Functions that need to be loaded on every Drupal request. @@ -718,13 +720,6 @@ function drupal_environment_initialize() { $_SERVER['HTTP_HOST'] = ''; } - // When clean URLs are enabled, emulate ?q=foo/bar using REQUEST_URI. It is - // not possible to append the query string using mod_rewrite without the B - // flag (this was added in Apache 2.2.8), because mod_rewrite unescapes the - // path before passing it on to PHP. This is a problem when the path contains - // e.g. "&" or "%" that have special meanings in URLs and must be encoded. - $_GET['q'] = request_path(); - // Enforce E_STRICT, but allow users to set levels not part of E_STRICT. error_reporting(E_STRICT | E_ALL | error_reporting()); @@ -2715,157 +2710,59 @@ function language_default() { } /** + * Returns the request object for the current request. + * + * @return Drupal\Core\Request + * The request object for this request, as populated from the PHP superglobals. + */ +function request() { + $request = &drupal_static(__FUNCTION__); + if (empty($request)) { + $request = Request::createFromGlobals(); + } + return $request; +} + +/** * Returns the requested URL path of the page being viewed. * - * Examples: - * - http://example.com/node/306 returns "node/306". - * - http://example.com/drupalfolder/node/306 returns "node/306" while - * base_path() returns "/drupalfolder/". - * - http://example.com/path/alias (which is a path alias for node/306) returns - * "path/alias" as opposed to the internal path. - * - http://example.com/index.php returns an empty string (meaning: front page). - * - http://example.com/index.php?page=1 returns an empty string. + * @deprecated * * @return * The requested Drupal URL path. * - * @see current_path() + * @see Drupal\Core\Request::requestPath() */ function request_path() { - static $path; - - if (isset($path)) { - return $path; - } - - if (isset($_GET['q'])) { - // This is a request with a ?q=foo/bar query string. $_GET['q'] is - // overwritten in drupal_path_initialize(), but request_path() is called - // very early in the bootstrap process, so the original value is saved in - // $path and returned in later calls. - $path = $_GET['q']; - } - elseif (isset($_SERVER['REQUEST_URI'])) { - // This request is either a clean URL, or 'index.php', or nonsense. - // Extract the path from REQUEST_URI. - $request_path = strtok($_SERVER['REQUEST_URI'], '?'); - $base_path_len = strlen(rtrim(dirname($_SERVER['SCRIPT_NAME']), '\/')); - // Unescape and strip $base_path prefix, leaving q without a leading slash. - $path = substr(urldecode($request_path), $base_path_len + 1); - // If the path equals the script filename, either because 'index.php' was - // explicitly provided in the URL, or because the server added it to - // $_SERVER['REQUEST_URI'] even when it wasn't provided in the URL (some - // versions of Microsoft IIS do this), the front page should be served. - if ($path == basename($_SERVER['PHP_SELF'])) { - $path = ''; - } - } - else { - // This is the front page. - $path = ''; - } - - // Under certain conditions Apache's RewriteRule directive prepends the value - // assigned to $_GET['q'] with a slash. Moreover we can always have a trailing - // slash in place, hence we need to normalize $_GET['q']. - $path = trim($path, '/'); - - return $path; + return request()->requestPath(); } /** * Returns a component of the current Drupal path. * - * When viewing a page at the path "admin/structure/types", for example, arg(0) - * returns "admin", arg(1) returns "structure", and arg(2) returns "types". - * - * Avoid use of this function where possible, as resulting code is hard to - * read. In menu callback functions, attempt to use named arguments. See the - * explanation in menu.inc for how to construct callbacks that take arguments. - * When attempting to use this function to load an element from the current - * path, e.g. loading the node on a node page, use menu_get_object() instead. - * - * @param $index - * The index of the component, where each component is separated by a '/' - * (forward-slash), and where the first component has an index of 0 (zero). - * @param $path - * A path to break into components. Defaults to the path of the current page. + * @deprecated * * @return * The component specified by $index, or NULL if the specified component was - * not found. If called without arguments, it returns an array containing all - * the components of the current path. + * not found. */ function arg($index = NULL, $path = NULL) { - // Even though $arguments doesn't need to be resettable for any functional - // reasons (the result of explode() does not depend on any run-time - // information), it should be resettable anyway in case a module needs to - // free up the memory used by it. - // Use the advanced drupal_static() pattern, since this is called very often. - static $drupal_static_fast; - if (!isset($drupal_static_fast)) { - $drupal_static_fast['arguments'] = &drupal_static(__FUNCTION__); - } - $arguments = &$drupal_static_fast['arguments']; - - if (!isset($path)) { - $path = $_GET['q']; - } - if (!isset($arguments[$path])) { - $arguments[$path] = explode('/', $path); - } - if (!isset($index)) { - return $arguments[$path]; - } - if (isset($arguments[$path][$index])) { - return $arguments[$path][$index]; - } + return request()->pathElement($index); } /** * Returns the IP address of the client machine. * - * If Drupal is behind a reverse proxy, we use the X-Forwarded-For header - * instead of $_SERVER['REMOTE_ADDR'], which would be the IP address of - * the proxy server, and not the client's. The actual header name can be - * configured by the reverse_proxy_header variable. + * @deprecated * * @return * IP address of client machine, adjusted for reverse proxy and/or cluster * environments. + * + * @see Symfony\Component\HttpFoundation\Request::getClientIp() */ function ip_address() { - $ip_address = &drupal_static(__FUNCTION__); - - if (!isset($ip_address)) { - $ip_address = $_SERVER['REMOTE_ADDR']; - - if (variable_get('reverse_proxy', 0)) { - $reverse_proxy_header = variable_get('reverse_proxy_header', 'HTTP_X_FORWARDED_FOR'); - if (!empty($_SERVER[$reverse_proxy_header])) { - // If an array of known reverse proxy IPs is provided, then trust - // the XFF header if request really comes from one of them. - $reverse_proxy_addresses = variable_get('reverse_proxy_addresses', array()); - - // Turn XFF header into an array. - $forwarded = explode(',', $_SERVER[$reverse_proxy_header]); - - // Trim the forwarded IPs; they may have been delimited by commas and spaces. - $forwarded = array_map('trim', $forwarded); - - // Tack direct client IP onto end of forwarded array. - $forwarded[] = $ip_address; - - // Eliminate all trusted IPs. - $untrusted = array_diff($forwarded, $reverse_proxy_addresses); - - // The right-most IP is the most specific we can trust. - $ip_address = array_pop($untrusted); - } - } - } - - return $ip_address; + return request()->getClientIp(variable_get('reverse_proxy', 0)); } /** diff --git a/core/includes/menu.inc b/core/includes/menu.inc index 40f9bfe..d0a7c88 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -445,7 +445,7 @@ function menu_get_item($path = NULL, $router_item = NULL) { if (variable_get('menu_rebuild_needed', FALSE) || !variable_get('menu_masks', array())) { menu_rebuild(); } - $original_map = arg(NULL, $path); + $original_map = explode('/', $path); // Since there is no limit to the length of $path, use a hash to keep it // short yet unique. @@ -1705,7 +1705,7 @@ function menu_get_active_help() { return ''; } - $arg = drupal_help_arg(arg(NULL)); + $arg = drupal_help_arg(explode('/', request()->systemPath())); foreach (module_implements('help') as $module) { $function = $module . '_help'; diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 40c4f35..9983a18 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -2332,7 +2332,7 @@ function template_preprocess_html(&$variables) { } // Populate the body classes. - if ($suggestions = theme_get_suggestions(arg(), 'page', '-')) { + if ($suggestions = theme_get_suggestions(request()->pathElements(), 'page', '-')) { foreach ($suggestions as $suggestion) { if ($suggestion != 'page-front') { // Add current suggestion to page classes to make it possible to theme @@ -2380,7 +2380,7 @@ function template_preprocess_html(&$variables) { $variables['head_title'] = implode(' | ', $head_title); // Populate the page template suggestions. - if ($suggestions = theme_get_suggestions(arg(), 'html')) { + if ($suggestions = theme_get_suggestions(request()->pathElements(), 'html')) { $variables['theme_hook_suggestions'] = $suggestions; } } @@ -2392,9 +2392,6 @@ function template_preprocess_html(&$variables) { * inside "modules/system/page.tpl.php". Look in there for the full list of * variables. * - * Uses the arg() function to generate a series of page template suggestions - * based on the current path. - * * Any changes to variables in this preprocessor should also be changed inside * template_preprocess_maintenance_page() to keep all of them consistent. * @@ -2439,7 +2436,7 @@ function template_preprocess_page(&$variables) { } // Populate the page template suggestions. - if ($suggestions = theme_get_suggestions(arg(), 'page')) { + if ($suggestions = theme_get_suggestions(request()->pathElements(), 'page')) { $variables['theme_hook_suggestions'] = $suggestions; } } @@ -2508,7 +2505,7 @@ function template_process_html(&$variables) { * base the additional suggestions on the path of the current page. * * @param $args - * An array of path arguments, such as from function arg(). + * An array of path arguments. * @param $base * A string identifying the base 'thing' from which more specific suggestions * are derived. For example, 'page' or 'html'. diff --git a/core/lib/Drupal/Core/Request.php b/core/lib/Drupal/Core/Request.php new file mode 100644 index 0000000..d514766 --- /dev/null +++ b/core/lib/Drupal/Core/Request.php @@ -0,0 +1,154 @@ +pathElements(); + return isset($elements[$index]) ? $elements[$index] : NULL; + } + + /** + * Returns the current path broken up into an array. + * + * @return array + * An array representing the elements of the path. + */ + public function pathElements() { + if (empty($this->pathElements)) { + $this->pathElements = explode('/', $this->systemPath()); + } + return $this->pathElements; + } + + /** + * Returns the requested URL path of the page being viewed. + * + * Examples: + * - http://example.com/node/306 returns "node/306". + * - http://example.com/drupalfolder/node/306 returns "node/306" while + * base_path() returns "/drupalfolder/". + * - http://example.com/path/alias (which is a path alias for node/306) returns + * "path/alias" as opposed to the internal path. + * - http://example.com/index.php returns an empty string (meaning: front page). + * - http://example.com/index.php?page=1 returns an empty string. + * + * @return + * The requested URL path, as it came from the user agent. + */ + public function requestPath() { + if (empty($this->requestPath)) { + $raw_path = ''; + + $q = $this->query->get('q'); + + if (!empty($q)) { + // This is a request with a ?q=foo/bar query string. That trumps all other + // path locations. + $raw_path = $q; + } + else { + // This request is either a clean URL, or 'index.php', or nonsense. + // Extract the path from REQUEST_URI. + $request_uri = $this->getRequestUri(); + $request_path = strtok($request_uri, '?'); + $script_name = $this->getScriptName(); + $base_path_len = strlen(rtrim(dirname($script_name), '\/')); + // Unescape and strip $base_path prefix, leaving q without a leading slash. + $raw_path = substr(urldecode($request_path), $base_path_len + 1); + // If the path equals the script filename, either because 'index.php' was + // explicitly provided in the URL, or because the server added it to + // $_SERVER['REQUEST_URI'] even when it wasn't provided in the URL (some + // versions of Microsoft IIS do this), the front page should be served. + $php_self = $this->server->get('PHP_SELF'); + if ($raw_path == basename($php_self)) { + $raw_path = ''; + } + } + + // Under certain conditions Apache's RewriteRule directive prepends the value + // assigned to $_GET['q'] with a slash. Moreover we can always have a trailing + // slash in place, hence we need to normalize $_GET['q']. + $this->requestPath = trim($raw_path, '/'); + } + + return $this->requestPath; + } + + /** + * Return the current URL path of the page being viewed. + * + * Examples: + * - http://example.com/node/306 returns "node/306". + * - http://example.com/drupalfolder/node/306 returns "node/306" while + * base_path() returns "/drupalfolder/". + * - http://example.com/path/alias (which is a path alias for node/306) returns + * "node/306" as opposed to the path alias. + * + * This function is not available in hook_boot() so use Request::requestPath() + * instead. Be aware that requestPath() does not have aliases resolved. + * + * @return + * The current URL path, with aliases resolved. + */ + public function systemPath() { + if (empty($this->systemPath)) { + // @todo Temporary hack. Fix when path is an object. + require_once DRUPAL_ROOT . '/core/includes/path.inc'; + + $path = $this->requestPath(); + + if (empty($path)) { + // @todo Temporary hack. Fix when configuration is injectable. + $path = variable_get('site_frontpage', 'node'); + } + $this->systemPath = drupal_get_normal_path($path); + } + + return $this->systemPath; + } +} diff --git a/core/modules/simpletest/simpletest.info b/core/modules/simpletest/simpletest.info index bbe65f0..8faf836 100644 --- a/core/modules/simpletest/simpletest.info +++ b/core/modules/simpletest/simpletest.info @@ -29,6 +29,7 @@ files[] = tests/module.test files[] = tests/password.test files[] = tests/path.test files[] = tests/registry.test +files[] = tests/request.test files[] = tests/schema.test files[] = tests/session.test files[] = tests/symfony.test diff --git a/core/modules/simpletest/tests/form_test.module b/core/modules/simpletest/tests/form_test.module index e1e2435..28ba566 100644 --- a/core/modules/simpletest/tests/form_test.module +++ b/core/modules/simpletest/tests/form_test.module @@ -1526,7 +1526,7 @@ function form_test_clicked_button($form, &$form_state) { // 'image_button', and a 'button' with #access=FALSE. This enables form.test // to test a variety of combinations. $i=0; - $args = array_slice(arg(), 2); + $args = array_slice(request()->pathElements(), 2); foreach ($args as $arg) { $name = 'button' . ++$i; // 's', 'b', or 'i' in the argument define the button type wanted. diff --git a/core/modules/simpletest/tests/request.test b/core/modules/simpletest/tests/request.test new file mode 100644 index 0000000..93e215d --- /dev/null +++ b/core/modules/simpletest/tests/request.test @@ -0,0 +1,92 @@ + 'Request', + 'description' => 'Tests for request information', + 'group' => 'Request', + ); + } + + public function setUp() { + parent::setUp(); + + require_once DRUPAL_ROOT . '/core/lib/Drupal/Core/Request.php'; + + // This unfortunately necessary because Drupal's registry throws a database + // exception when testing class_exist on non-existent classes in unit tests. + // This is the point of one of our tests so we have to remove the registry + // prior to running our tests. + spl_autoload_unregister('drupal_autoload_class'); + spl_autoload_unregister('drupal_autoload_interface'); + } + + public function tearDown() { + // Re-register drupal's autoload classes. See setUp for the reasoning. + spl_autoload_register('drupal_autoload_class'); + spl_autoload_register('drupal_autoload_interface'); + + parent::tearDown(); + } + + /** + * Test the request path logic. + */ + function testRequestPathFromPath() { + $request = Request::create('/' . $this->testPath); + + $this->assertEqual($request->requestPath(), $this->testPath, t('Correct request path derived from path.')); + } + + /** + * Test the request path logic. + */ + function testRequestPathFromQuery() { + $request = Request::create('index.php', 'GET', array('q' => $this->testPath)); + + $this->assertEqual($request->requestPath(), $this->testPath, t('Correct request path derived from arguments.')); + } + + /** + * Test the path fragment logic. + */ + function testPathFragment() { + $request = Request::create('/' . $this->testPath); + + $this->assertEqual($request->pathElement(0), 'foo', t('Correct first path element returned.')); + $this->assertEqual($request->pathElement(1), 'bar', t('Correct second path element returned.')); + $this->assertNull($request->pathElement(2), t('Null returned for non-existent path element.')); + } + + /** + * Test the path fragment logic. + */ + function testPathFragments() { + $request = Request::create('/' . $this->testPath); + + $elements = $request->pathElements(); + + $this->assertEqual($elements[0], 'foo', t('Correct first path element returned.')); + $this->assertEqual($elements[1], 'bar', t('Correct second path element returned.')); + $this->assertTrue(empty($elements[2]), t('Null returned for non-existent path element.')); + } + + + // System path cannot be unit tested, because it relies on the database. + // @TODO: Add a unit test here once the path logic becomes an object and can + // be mocked. + +}