diff --git a/system/HTTP/SiteURI.php b/system/HTTP/SiteURI.php new file mode 100644 index 000000000000..bf3e06903188 --- /dev/null +++ b/system/HTTP/SiteURI.php @@ -0,0 +1,312 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP; + +use BadMethodCallException; +use CodeIgniter\HTTP\Exceptions\HTTPException; +use Config\App; + +/** + * URI for the application site + */ +class SiteURI extends URI +{ + /** + * The baseURL. + */ + private string $baseURL; + + /** + * The Index File. + */ + private string $indexPage; + + /** + * List of URI segments in baseURL and indexPage. + * + * If the URI is "http://localhost:8888/ci431/public/index.php/test?a=b", + * and the baseUR is "http://localhost:8888/ci431/public/", then: + * $baseSegments = [ + * 0 => 'ci431', + * 1 => 'public', + * 2 => 'index.php', + * ]; + */ + private array $baseSegments; + + /** + * List of URI segments after indexPage. + * + * The word "URI Segments" originally means only the URI path part relative + * to the baseURL. + * + * If the URI is "http://localhost:8888/ci431/public/index.php/test?a=b", + * and the baseUR is "http://localhost:8888/ci431/public/", then: + * $segments = [ + * 0 => 'test', + * ]; + * + * @var array + * + * @deprecated This property will be private. + */ + protected $segments; + + /** + * URI path relative to baseURL. + * + * If the baseURL contains sub folders, this value will be different from + * the current URI path. + */ + private string $routePath; + + public function __construct(App $configApp) + { + // It's possible the user forgot a trailing slash on their + // baseURL, so let's help them out. + $baseURL = rtrim($configApp->baseURL, '/ ') . '/'; + + $this->baseURL = $baseURL; + $this->indexPage = $configApp->indexPage; + + $this->setBaseSeegments(); + + // Check for an index page + $indexPage = ''; + if ($configApp->indexPage !== '') { + $indexPage = $configApp->indexPage . '/'; + } + + $tempUri = $this->baseURL . $indexPage; + $uri = new URI($tempUri); + + if ($configApp->forceGlobalSecureRequests) { + $uri->setScheme('https'); + } + + $parts = parse_url((string) $uri); + if ($parts === false) { + throw HTTPException::forUnableToParseURI($uri); + } + $this->applyParts($parts); + + $this->setPath('/'); + } + + /** + * Sets baseSegments. + */ + private function setBaseSeegments(): void + { + $basePath = (new URI($this->baseURL))->getPath(); + $this->baseSegments = $this->convertToSegments($basePath); + + if ($this->indexPage) { + $this->baseSegments[] = $this->indexPage; + } + } + + public function setURI(?string $uri = null) + { + throw new BadMethodCallException('Cannot use this method.'); + } + + /** + * Returns the URI path relative to baseURL. + * + * @return string The Route path. + */ + public function getRoutePath(): string + { + return $this->routePath; + } + + /** + * Returns the URI segments of the path as an array. + */ + public function getSegments(): array + { + return $this->segments; + } + + /** + * Returns the value of a specific segment of the URI path relative to baseURL. + * + * @param int $number Segment number + * @param string $default Default value + * + * @return string The value of the segment. If no segment is found, + * throws HTTPException + */ + public function getSegment(int $number, string $default = ''): string + { + if ($number < 1) { + throw HTTPException::forURISegmentOutOfRange($number); + } + + if ($number > count($this->segments) && ! $this->silent) { + throw HTTPException::forURISegmentOutOfRange($number); + } + + // The segment should treat the array as 1-based for the user + // but we still have to deal with a zero-based array. + $number--; + + return $this->segments[$number] ?? $default; + } + + /** + * Set the value of a specific segment of the URI path relative to baseURL. + * Allows to set only existing segments or add new one. + * + * @param int $number The segment number. Starting with 1. + * @param string $value The segment value. + * + * @return $this + */ + public function setSegment(int $number, $value) + { + if ($number < 1) { + throw HTTPException::forURISegmentOutOfRange($number); + } + + if ($number > count($this->segments) + 1) { + if ($this->silent) { + return $this; + } + + throw HTTPException::forURISegmentOutOfRange($number); + } + + // The segment should treat the array as 1-based for the user, + // but we still have to deal with a zero-based array. + $number--; + + $this->segments[$number] = $value; + + $this->refreshPath(); + + return $this; + } + + /** + * Returns the total number of segments. + */ + public function getTotalSegments(): int + { + return count($this->segments); + } + + /** + * Formats the URI as a string. + */ + public function __toString(): string + { + return static::createURIString( + $this->getScheme(), + $this->getAuthority(), + $this->getPath(), // Absolute URIs should use a "/" for an empty path + $this->getQuery(), + $this->getFragment() + ); + } + + /** + * Sets the route path (and segments). + * + * @return $this + */ + public function setPath(string $path) + { + $this->routePath = $this->filterPath($path); + + $this->segments = $this->convertToSegments($this->routePath); + + $this->refreshPath(); + + return $this; + } + + /** + * Converts path to segments + */ + private function convertToSegments(string $path): array + { + $tempPath = trim($path, '/'); + + return ($tempPath === '') ? [] : explode('/', $tempPath); + } + + /** + * Sets the path portion of the URI based on segments. + * + * @return $this + * + * @deprecated This method will be private. + */ + public function refreshPath() + { + $allSegments = array_merge($this->baseSegments, $this->segments); + $this->path = '/' . $this->filterPath(implode('/', $allSegments)); + + $this->routePath = $this->filterPath(implode('/', $this->segments)); + + if ($this->routePath === '') { + $this->routePath = '/'; + + if ($this->indexPage !== '') { + $this->path .= '/'; + } + } + + return $this; + } + + /** + * Saves our parts from a parse_url() call. + */ + protected function applyParts(array $parts) + { + if (! empty($parts['host'])) { + $this->host = $parts['host']; + } + if (! empty($parts['user'])) { + $this->user = $parts['user']; + } + if (isset($parts['path']) && $parts['path'] !== '') { + $this->path = $this->filterPath($parts['path']); + } + if (! empty($parts['query'])) { + $this->setQuery($parts['query']); + } + if (! empty($parts['fragment'])) { + $this->fragment = $parts['fragment']; + } + + // Scheme + if (isset($parts['scheme'])) { + $this->setScheme(rtrim($parts['scheme'], ':/')); + } else { + $this->setScheme('http'); + } + + // Port + if (isset($parts['port']) && $parts['port'] !== null) { + // Valid port numbers are enforced by earlier parse_url() or setPort() + $this->port = $parts['port']; + } + + if (isset($parts['pass'])) { + $this->password = $parts['pass']; + } + } +} diff --git a/system/HTTP/URI.php b/system/HTTP/URI.php index 587e441ffc17..5fb5190184cd 100644 --- a/system/HTTP/URI.php +++ b/system/HTTP/URI.php @@ -785,6 +785,8 @@ public function getBaseURL(): string * Sets the path portion of the URI based on segments. * * @return $this + * + * @deprecated This method will be private. */ public function refreshPath() { diff --git a/tests/system/HTTP/SiteURITest.php b/tests/system/HTTP/SiteURITest.php new file mode 100644 index 000000000000..3a7c532519f6 --- /dev/null +++ b/tests/system/HTTP/SiteURITest.php @@ -0,0 +1,245 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\HTTP; + +use BadMethodCallException; +use CodeIgniter\HTTP\Exceptions\HTTPException; +use CodeIgniter\Test\CIUnitTestCase; +use Config\App; + +/** + * @backupGlobals enabled + * + * @internal + * + * @group Others + */ +final class SiteURITest extends CIUnitTestCase +{ + public function testConstructor() + { + $config = new App(); + + $uri = new SiteURI($config); + + $this->assertInstanceOf(SiteURI::class, $uri); + $this->assertSame('http://example.com/index.php/', (string) $uri); + $this->assertSame('/index.php/', $uri->getPath()); + } + + public function testConstructorSubfolder() + { + $config = new App(); + $config->baseURL = 'http://example.com/ci4/'; + + $uri = new SiteURI($config); + + $this->assertInstanceOf(SiteURI::class, $uri); + $this->assertSame('http://example.com/ci4/index.php/', (string) $uri); + $this->assertSame('/ci4/index.php/', $uri->getPath()); + } + + public function testConstructorForceGlobalSecureRequests() + { + $config = new App(); + $config->forceGlobalSecureRequests = true; + + $uri = new SiteURI($config); + + $this->assertSame('https://example.com/index.php/', (string) $uri); + } + + public function testConstructorIndexPageEmpty() + { + $config = new App(); + $config->indexPage = ''; + + $uri = new SiteURI($config); + + $this->assertSame('http://example.com/', (string) $uri); + } + + public function testSetPath() + { + $config = new App(); + + $uri = new SiteURI($config); + + $uri->setPath('test/method'); + + $this->assertSame('http://example.com/index.php/test/method', (string) $uri); + $this->assertSame('test/method', $uri->getRoutePath()); + $this->assertSame('/index.php/test/method', $uri->getPath()); + $this->assertSame(['test', 'method'], $uri->getSegments()); + $this->assertSame('test', $uri->getSegment(1)); + $this->assertSame(2, $uri->getTotalSegments()); + } + + public function testSetPathSubfolder() + { + $config = new App(); + $config->baseURL = 'http://example.com/ci4/'; + + $uri = new SiteURI($config); + + $uri->setPath('test/method'); + + $this->assertSame('http://example.com/ci4/index.php/test/method', (string) $uri); + $this->assertSame('test/method', $uri->getRoutePath()); + $this->assertSame('/ci4/index.php/test/method', $uri->getPath()); + $this->assertSame(['test', 'method'], $uri->getSegments()); + $this->assertSame('test', $uri->getSegment(1)); + $this->assertSame(2, $uri->getTotalSegments()); + } + + public function testSetPathEmpty() + { + $config = new App(); + + $uri = new SiteURI($config); + + $uri->setPath(''); + + $this->assertSame('http://example.com/index.php/', (string) $uri); + $this->assertSame('/', $uri->getRoutePath()); + $this->assertSame('/index.php/', $uri->getPath()); + $this->assertSame([], $uri->getSegments()); + $this->assertSame(0, $uri->getTotalSegments()); + } + + public function testSetSegment() + { + $config = new App(); + + $uri = new SiteURI($config); + $uri->setPath('test/method'); + + $uri->setSegment(1, 'one'); + + $this->assertSame('http://example.com/index.php/one/method', (string) $uri); + $this->assertSame('one/method', $uri->getRoutePath()); + $this->assertSame('/index.php/one/method', $uri->getPath()); + $this->assertSame(['one', 'method'], $uri->getSegments()); + $this->assertSame('one', $uri->getSegment(1)); + $this->assertSame(2, $uri->getTotalSegments()); + } + + public function testSetSegmentOutOfRange() + { + $this->expectException(HTTPException::class); + + $config = new App(); + $uri = new SiteURI($config); + $uri->setPath('test/method'); + + $uri->setSegment(4, 'four'); + } + + public function testSetSegmentSilentOutOfRange() + { + $config = new App(); + $uri = new SiteURI($config); + $uri->setPath('one/method'); + $uri->setSilent(); + + $uri->setSegment(4, 'four'); + $this->assertSame(['one', 'method'], $uri->getSegments()); + } + + public function testSetSegmentZero() + { + $this->expectException(HTTPException::class); + + $config = new App(); + $uri = new SiteURI($config); + $uri->setPath('test/method'); + + $uri->setSegment(0, 'four'); + } + + public function testSetSegmentSubfolder() + { + $config = new App(); + $config->baseURL = 'http://example.com/ci4/'; + + $uri = new SiteURI($config); + $uri->setPath('test/method'); + + $uri->setSegment(1, 'one'); + + $this->assertSame('http://example.com/ci4/index.php/one/method', (string) $uri); + $this->assertSame('one/method', $uri->getRoutePath()); + $this->assertSame('/ci4/index.php/one/method', $uri->getPath()); + $this->assertSame(['one', 'method'], $uri->getSegments()); + $this->assertSame('one', $uri->getSegment(1)); + $this->assertSame(2, $uri->getTotalSegments()); + } + + public function testGetRoutePath() + { + $config = new App(); + $uri = new SiteURI($config); + + $this->assertSame('/', $uri->getRoutePath()); + } + + public function testGetSegments() + { + $config = new App(); + $uri = new SiteURI($config); + + $this->assertSame([], $uri->getSegments()); + } + + public function testGetSegmentZero() + { + $this->expectException(HTTPException::class); + + $config = new App(); + $uri = new SiteURI($config); + $uri->setPath('test/method'); + + $uri->getSegment(0); + } + + public function testGetSegmentOutOfRange() + { + $this->expectException(HTTPException::class); + + $config = new App(); + $uri = new SiteURI($config); + $uri->setPath('test/method'); + + $this->assertSame('method', $uri->getSegment(2)); + $this->assertSame('', $uri->getSegment(3)); + + $uri->getSegment(4); + } + + public function testGetTotalSegments() + { + $config = new App(); + $uri = new SiteURI($config); + + $this->assertSame(0, $uri->getTotalSegments()); + } + + public function testSetURI() + { + $this->expectException(BadMethodCallException::class); + + $config = new App(); + $uri = new SiteURI($config); + + $uri->setURI('http://another.site.example.jp/'); + } +}