Skip to content

Commit a713d51

Browse files
committed
Refactor URI detection
1 parent 7546ee0 commit a713d51

File tree

5 files changed

+174
-40
lines changed

5 files changed

+174
-40
lines changed

system/CodeIgniter.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ protected function handleRequest(?RouteCollectionInterface $routes, Cache $cache
400400
$filters->enableFilter($routeFilter, 'after');
401401
}
402402

403-
$uri = $this->request instanceof CLIRequest ? $this->request->getPath() : $this->request->getUri()->getPath();
403+
$uri = $this->determinePath();
404404

405405
// Never run filters when running through Spark cli
406406
if (! defined('SPARKED'))
@@ -846,8 +846,7 @@ protected function determinePath()
846846
return $this->path;
847847
}
848848

849-
// @phpstan-ignore-next-line
850-
return (is_cli() && ! (ENVIRONMENT === 'testing')) ? $this->request->getPath() : $this->request->uri->getPath();
849+
return method_exists($this->request, 'getPath') ? $this->request->getPath() : $this->request->getUri()->getPath();
851850
}
852851

853852
//--------------------------------------------------------------------

system/HTTP/IncomingRequest.php

Lines changed: 89 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ class IncomingRequest extends Request
6262
*/
6363
public $uri;
6464

65+
/**
66+
* The detected path (relative to SCRIPT_NAME).
67+
* Note: current_url() uses this to build its URI,
68+
* so this becomes the source for the "current URL"
69+
* when working with the share request instance.
70+
*
71+
* @var string|null
72+
*/
73+
protected $path;
74+
6575
/**
6676
* File collection
6777
*
@@ -159,12 +169,6 @@ public function __construct($config, URI $uri = null, $body = 'php://input', Use
159169

160170
$this->detectURI($config->uriProtocol, $config->baseURL);
161171

162-
// Check if the baseURL scheme needs to be coerced into its secure version
163-
if ($config->forceGlobalSecureRequests && $this->uri->getScheme() === 'http')
164-
{
165-
$this->uri->setScheme('https');
166-
}
167-
168172
$this->validLocales = $config->supportedLocales;
169173

170174
$this->detectLocale($config);
@@ -613,11 +617,34 @@ public function getFile(string $fileID)
613617
*/
614618
protected function detectURI(string $protocol, string $baseURL)
615619
{
616-
$this->uri->setPath($this->detectPath($protocol));
620+
// Passing the config is unnecessary but left for legacy purposes
621+
$config = clone $this->config;
622+
$config->baseURL = $baseURL;
623+
624+
$this->setPath($this->detectPath($protocol), $config);
625+
}
626+
627+
/**
628+
* Sets the relative path and updates the URI object.
629+
* Note: Since current_url() accesses the shared request
630+
* instance, this can be used to change the "current URL"
631+
* for testing.
632+
*
633+
* @param string $path URI path relative to SCRIPT_NAME
634+
* @param App $config Optional alternate config to use
635+
*
636+
* @return $this
637+
*/
638+
public function setPath(string $path, App $config = null)
639+
{
640+
$this->path = $path;
641+
$this->uri->setPath($path);
642+
643+
$config = $config ?? $this->config;
617644

618645
// It's possible the user forgot a trailing slash on their
619646
// baseURL, so let's help them out.
620-
$baseURL = $baseURL === '' ? $baseURL : rtrim($baseURL, '/ ') . '/';
647+
$baseURL = $config->baseURL === '' ? $config->baseURL : rtrim($config->baseURL, '/ ') . '/';
621648

622649
// Based on our baseURL provided by the developer
623650
// set our current domain name, scheme
@@ -629,23 +656,44 @@ protected function detectURI(string $protocol, string $baseURL)
629656

630657
// Ensure we have any query vars
631658
$this->uri->setQuery($_SERVER['QUERY_STRING'] ?? '');
632-
}
633-
else
634-
{
635-
// @codeCoverageIgnoreStart
636-
if (! is_cli())
659+
660+
// Check if the baseURL scheme needs to be coerced into its secure version
661+
if ($config->forceGlobalSecureRequests && $this->uri->getScheme() === 'http')
637662
{
638-
die('You have an empty or invalid base URL. The baseURL value must be set in Config\App.php, or through the .env file.');
663+
$this->uri->setScheme('https');
639664
}
640-
// @codeCoverageIgnoreEnd
641665
}
666+
// @codeCoverageIgnoreStart
667+
elseif (! is_cli())
668+
{
669+
die('You have an empty or invalid base URL. The baseURL value must be set in Config\App.php, or through the .env file.');
670+
}
671+
// @codeCoverageIgnoreEnd
672+
673+
return $this;
642674
}
643675

644676
//--------------------------------------------------------------------
645677

646678
/**
647-
* Based on the URIProtocol Config setting, will attempt to
648-
* detect the path portion of the current URI.
679+
* Returns the path relative to SCRIPT_NAME,
680+
* running detection as necessary.
681+
*
682+
* @return string
683+
*/
684+
public function getPath(): string
685+
{
686+
if (is_null($this->path))
687+
{
688+
$this->detectPath($this->config->uriProtocol);
689+
}
690+
691+
return $this->path;
692+
}
693+
694+
/**
695+
* Detects the relative path based on
696+
* the URIProtocol Config setting.
649697
*
650698
* @param string $protocol
651699
*
@@ -661,18 +709,18 @@ public function detectPath(string $protocol = ''): string
661709
switch ($protocol)
662710
{
663711
case 'REQUEST_URI':
664-
$path = $this->parseRequestURI();
712+
$this->path = $this->parseRequestURI();
665713
break;
666714
case 'QUERY_STRING':
667-
$path = $this->parseQueryString();
715+
$this->path = $this->parseQueryString();
668716
break;
669717
case 'PATH_INFO':
670718
default:
671-
$path = $this->fetchGlobal('server', $protocol) ?? $this->parseRequestURI();
719+
$this->path = $this->fetchGlobal('server', $protocol) ?? $this->parseRequestURI();
672720
break;
673721
}
674722

675-
return $path;
723+
return $this->path;
676724
}
677725

678726
//--------------------------------------------------------------------
@@ -731,23 +779,23 @@ protected function parseRequestURI(): string
731779
$query = $parts['query'] ?? '';
732780
$uri = $parts['path'] ?? '';
733781

734-
if (isset($_SERVER['SCRIPT_NAME'][0]) && pathinfo($_SERVER['SCRIPT_NAME'], PATHINFO_EXTENSION) === 'php')
782+
// Strip the SCRIPT_NAME path from the URI
783+
if ($uri !== '' && isset($_SERVER['SCRIPT_NAME'][0]) && pathinfo($_SERVER['SCRIPT_NAME'], PATHINFO_EXTENSION) === 'php')
735784
{
736-
// strip the script name from the beginning of the URI
737-
if (strpos($uri, $_SERVER['SCRIPT_NAME']) === 0 && strpos($uri, '/index.php') === 0)
738-
{
739-
$uri = (string) substr($uri, strlen($_SERVER['SCRIPT_NAME']));
740-
}
741-
// if the script is nested, strip the parent folder & script from the URI
742-
elseif (strpos($uri, $_SERVER['SCRIPT_NAME']) > 0)
743-
{
744-
$uri = (string) substr($uri, strpos($uri, $_SERVER['SCRIPT_NAME']) + strlen($_SERVER['SCRIPT_NAME']));
745-
}
746-
// or if index.php is implied
747-
elseif (strpos($uri, dirname($_SERVER['SCRIPT_NAME'])) === 0)
785+
// Compare each segment, dropping them until there is no match
786+
$segments = $keep = explode('/', $uri);
787+
foreach (explode('/', $_SERVER['SCRIPT_NAME']) as $i => $segment)
748788
{
749-
$uri = (string) substr($uri, strlen(dirname($_SERVER['SCRIPT_NAME'])));
789+
// If these segments are not the same then we're done
790+
if ($segment !== $segments[$i])
791+
{
792+
break;
793+
}
794+
795+
array_shift($keep);
750796
}
797+
798+
$uri = implode('/', $keep);
751799
}
752800

753801
// This section ensures that even on servers that require the URI to contain the query string (Nginx) a correct
@@ -763,7 +811,10 @@ protected function parseRequestURI(): string
763811
$_SERVER['QUERY_STRING'] = $query;
764812
}
765813

814+
// Update our globals for values likely to been have changed
766815
parse_str($_SERVER['QUERY_STRING'], $_GET);
816+
$this->populateGlobals('server');
817+
$this->populateGlobals('get');
767818

768819
$uri = URI::removeDotSegments($uri);
769820

@@ -795,7 +846,10 @@ protected function parseQueryString(): string
795846
$uri = $uri[0];
796847
}
797848

849+
// Update our globals for values likely to been have changed
798850
parse_str($_SERVER['QUERY_STRING'], $_GET);
851+
$this->populateGlobals('server');
852+
$this->populateGlobals('get');
799853

800854
$uri = URI::removeDotSegments($uri);
801855

tests/system/HTTP/IncomingRequestDetectingTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public function testPathRequestURISubfolder()
6767
{
6868
$this->request->uri = '/ci/index.php/popcorn/woot?code=good#pos';
6969
$_SERVER['REQUEST_URI'] = '/ci/index.php/popcorn/woot';
70-
$_SERVER['SCRIPT_NAME'] = '/index.php';
70+
$_SERVER['SCRIPT_NAME'] = '/ci/index.php';
7171
$expected = 'popcorn/woot';
7272
$this->assertEquals($expected, $this->request->detectPath('REQUEST_URI'));
7373
}

tests/system/HTTP/IncomingRequestTest.php

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,7 @@ public function providePathChecks()
594594
return [
595595
'not /index.php' => [
596596
'/test.php',
597-
'test.php',
597+
'/',
598598
],
599599
'/index.php' => [
600600
'/index.php',
@@ -616,4 +616,71 @@ public function testExtensionPHP($path, $detectPath)
616616
$request = new IncomingRequest($config, new URI($path), null, new UserAgent());
617617
$this->assertEquals($detectPath, $request->detectPath());
618618
}
619+
620+
//--------------------------------------------------------------------
621+
622+
public function testGetPath()
623+
{
624+
$_SERVER['REQUEST_URI'] = '/index.php/fruits/banana';
625+
$_SERVER['SCRIPT_NAME'] = '/index.php';
626+
627+
$request = new IncomingRequest(new App(), new URI(), null, new UserAgent());
628+
629+
$this->assertEquals('fruits/banana', $request->getPath());
630+
}
631+
632+
public function testGetPathIsRelative()
633+
{
634+
$_SERVER['REQUEST_URI'] = '/sub/folder/index.php/fruits/banana';
635+
$_SERVER['SCRIPT_NAME'] = '/sub/folder/index.php';
636+
637+
$request = new IncomingRequest(new App(), new URI(), null, new UserAgent());
638+
639+
$this->assertEquals('fruits/banana', $request->getPath());
640+
}
641+
642+
public function testGetPathStoresDetectedValue()
643+
{
644+
$_SERVER['REQUEST_URI'] = '/fruits/banana';
645+
$_SERVER['SCRIPT_NAME'] = '/index.php';
646+
647+
$request = new IncomingRequest(new App(), new URI(), null, new UserAgent());
648+
649+
$_SERVER['REQUEST_URI'] = '/candy/snickers';
650+
651+
$this->assertEquals('fruits/banana', $request->getPath());
652+
}
653+
654+
public function testGetPathIsRediscovered()
655+
{
656+
$_SERVER['REQUEST_URI'] = '/fruits/banana';
657+
$_SERVER['SCRIPT_NAME'] = '/index.php';
658+
659+
$request = new IncomingRequest(new App(), new URI(), null, new UserAgent());
660+
661+
$_SERVER['REQUEST_URI'] = '/candy/snickers';
662+
$request->detectPath();
663+
664+
$this->assertEquals('candy/snickers', $request->getPath());
665+
}
666+
667+
//--------------------------------------------------------------------
668+
669+
public function testSetPath()
670+
{
671+
$request = new IncomingRequest(new App(), new URI(), null, new UserAgent());
672+
$this->assertEquals('', $request->getPath());
673+
674+
$request->setPath('foobar');
675+
$this->assertEquals('foobar', $request->getPath());
676+
}
677+
678+
public function testSetPathUpdatesURI()
679+
{
680+
$request = new IncomingRequest(new App(), new URI(), null, new UserAgent());
681+
682+
$request->setPath('apples');
683+
684+
$this->assertEquals('apples', $request->uri->getPath());
685+
}
619686
}

user_guide_src/source/incoming/incomingrequest.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,20 @@ The object gives you full abilities to grab any part of the request on it's own:
273273
echo $uri->getSegment(1); // 'path'
274274
echo $uri->getTotalSegments(); // 3
275275

276+
You can work with the current URI string (the path relative to your baseURL) using the ``getPath()`` and ``setPath()`` methods.
277+
Note that this relative path on the shared instance of ``IncomingRequest`` is what the :doc:`URL Helper </helpers/url_helper>`
278+
functions use, so this is a helpful way to "spoof" an incoming request for testing::
279+
280+
class MyMenuTest extends CIUnitTestCase
281+
{
282+
public function testActiveLinkUsesCurrentUrl()
283+
{
284+
service('request')->setPath('users/list');
285+
$menu = new MyMenu();
286+
$this->assertTrue('users/list', $menu->getActiveLink());
287+
}
288+
}
289+
276290
Uploaded Files
277291
--------------
278292

0 commit comments

Comments
 (0)