Repository: codeigniter4/CodeIgniter4 (PHP)

Issue:
Bug: codeigniter in a subdirectory + indexPage results in 404
### PHP Version

8.3

### CodeIgniter4 Version

4.6.1

### CodeIgniter4 Installation Method

Composer (using `codeigniter4/appstarter`)

### Which operating systems have you tested for this bug?

Linux

### Which server did you use?

apache

### Database

_No response_

### What happened?

I have a codeigniter 4 app that has to live in a shared hosting enviroment and has to be in a subdirectory.
The app has been developed with legacy autorouting enabled.
The app has indexPage as `index.php`

All the autorouting routes end up throwing a 404 with the "index.php is not a valid controller name" statement.

### Steps to Reproduce

1. get an apache webserver
2. set `indexPage ` as `index.php` in your app/Config/App.php
3. install codeigniter in a subdirectory:
```
/var/www/html/code-igniger-app
/var/www/html/other-apps-here
```
this way you'll access the app from http://localhost:8080/code-igniger-app/index.php
4. enable legacy autorouting and create a controller

### Expected Output

It should not be throwing a 404

### Anything else?

To fix it i added the following code to the very beginning of the index.php
```php
$_SERVER["SCRIPT_NAME"] = str_replace("/public","",$_SERVER["SCRIPT_NAME"]);
```
The SCRIPT_NAME would have been: `/code-igniger-app/public/index.php` this way it becomes `/code-igniger-app/index.php`.
This way when `CodeIgniter\HTTP\SiteURIFactory` uses the `parseRequestURI` method to remove the `SCRIPT_NAME` form the uri and then return the `$path` it is able to return it correctly.


Discussion:
It would be nice to be able for it to work without that line on the index.php (I really dislike it).
Should I make a pr where I fix it?
This isn't a CodeIgniter issue - it's related to your server configuration.

Please refer to the user guide for more details:

- [Hosting with Apache](https://codeigniter.com/user_guide/installation/running.html#hosting-with-apache)
- [Deployment to Shared Hosting](https://codeigniter.com/user_guide/installation/deployment.html#deployment-to-shared-hosting-services)  

If you need further help, feel free to post on our [forum](https://forum.codeigniter.com/).

Hello @michalsn
I opened a thread on the forum: https://forum.codeigniter.com/showthread.php?tid=93012&pid=425896#pid425896

I am still quite sure that's a bug.

The apache webserver is configured as so:
```apache
<VirtualHost *:8080>
ServerAdmin francesco@ciannavei.dev
DocumentRoot /var/www/html

ErrorLog ${APACHE_LOG_DIR}/html/error.log
CustomLog ${APACHE_LOG_DIR}/html/access.log combined
</VirtualHost>
```


This is the /var/www/html directory:
```bash
ls

code-igniter-app
index.html
react-app
shared-data
```


Inside the code-igniter-app directory I have the following .htaccess file in the root of the project (/var/www/html/code-igniter-app):
```apache
<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteRule ^(.*)$ public/$1 [L]
</IfModule>

<FilesMatch "^\.">
    Require all denied
    Satisfy All
</FilesMatch>
```
I had this .htaccess before I opened the issue:
```apache
RewriteEngine On

DirectorySlash On

RewriteCond %{REQUEST_URI} !^/code-igniter-app/public/
RewriteRule .* /code-igniter-app/public/$0 [L]

DirectoryIndex index.php
```


The codeigniter app is configured as follows (these are the configurations i edited from the default ones/the relevant ones):
/Config/App.php
```php
public string $indexPage = 'index.php';
```
/Config/Routing.php
```php
public bool $autoRoute = true;
```
/Config/Feature.php
```php
public bool $autoRoutesImproved = false;
```


The problem is that the project is installed in a directory and the `$indexPage` is set as `index.php`. the `SiteURIFactory` is not taking those option into consideration.

Should I create a pr to show you where the problem is?

Thanks for your time.
Ok. It looks like you are now doing everything as you should. It might be a bug. I'll try to work on it over the weekend, since I don't have Apache on my main computer.

Although it would be a very specific bug for: legacy routing, served from a subfolder, and with index.php... what a combo.
@michalsn 
I would like to help, I believe I already fixed it, I was testing my fix earlier today (I used [cweagans composer patches](https://github.com/cweagans/composer-patches) to deploy it).
If you're ok with that I can create a pull request.

Feel free to send a PR, but please also include some tests.
After reviewing your report, I believe the limitation here is how `SCRIPT_NAME` is handled by PHP in this particular setup. Since `SCRIPT_NAME` reflects the server environment and is not designed to be programmatically altered, relying on it for routing within subdirectories (while keeping `index.php` visible) leads to issues that aren’t easy for the framework to manage reliably.

In this context, using `index.php` in the URL while serving the app from a subfolder does create issues that the framework can’t easily resolve without hardcoding assumptions (such as the exact name or position of the public directory), which we want to avoid for flexibility and portability.

The most robust and supported deployment method in this case is to follow the two-directory setup recommended in the user guide: https://codeigniter.com/user_guide/installation/deployment.html#using-two-directories

This approach avoids all ambiguity around routing, ensures security by keeping non-public files outside the web root, and doesn’t require workarounds or directory rewrites. It’s also more maintainable in the long run, especially in shared hosting environments.

That said, if you have a clean proposal that improves compatibility with subdirectory setups using `index.php`, without making assumptions about the directory structure, we’d be happy to review a pull request.
@michalsn I do, I created this commit on my fork: https://github.com/Franky5831/CodeIgniter4/commit/1bb8f524982cef6a814d070550efe11d3d61966b
it basically removes `/public` from the script name when it tries to perform the routing.

I am still testing the change and writing some tests.
This is my first time writing tests so it's taking me a while.
To be honest, it does not look good. The folder name is hard-coded, which is unacceptable. I haven't tested it, but I doubt that our tests will pass after your changes.

I would suggest switching to the proposed solution instead of trying to solve this problem. We shouldn't sacrifice code simplicity and readability to handle some edge cases, especially since serving an application from a subfolder is not recommended in production anyway.

It may sound a little harsh, but I don't want you to waste your time.
I understand, I rewrote the fix on my fork, I did it over 4 commits: https://github.com/Franky5831/CodeIgniter4/commit/4343fe4d35384257215ea9ab225d1607235a42c4, https://github.com/Franky5831/CodeIgniter4/commit/89ce426f88c8e3d7008d89c63c0d23ee95cfbe3a, https://github.com/Franky5831/CodeIgniter4/commit/74ef94a63ad3fc54f730d23546e12695f77856ac, https://github.com/Franky5831/CodeIgniter4/commit/9063df578d9739bf6467856be5df2808540929aa

I ran the CodeIgniter tests excluding the database and cache tests with the following command: `vendor/bin/phpunit --exclude-group DatabaseLive --exclude-group CacheLive` and it passed all tests.
Should I create a pull request so you can better judge what I have done?

Also, this is not a waste of my time, I am still learning new things about the framework as I make changes and try new things. If you feel like I am wasting your time please let me know, I'll just use the [composer-patches](https://github.com/cweagans/composer-patches) to patch the app I am working on. Unfortunately I cannot use virtual hosts, I have no control over the apache configs, I can just "add stuff" a shared directory and the app has to have "index.php"
To use the "two-directory approach", you don't need access to the server config or create a virtual host. You only need access to the level above the public folder on your server.

As I said before, you can send a PR - we always appreciate user contributions.

Please follow our contribution guidelines - it will help you with your first PR: https://github.com/codeigniter4/CodeIgniter4/blob/develop/contributing/pull_request.md



Patch:
diff --git a/system/HTTP/SiteURIFactory.php b/system/HTTP/SiteURIFactory.php
index c87898c0470e..2c94f23f4678 100644
--- a/system/HTTP/SiteURIFactory.php
+++ b/system/HTTP/SiteURIFactory.php
@@ -146,6 +146,15 @@ private function parseRequestURI(): string
             $path = implode('/', $keep);
         }
 
+        // Cleanup: if indexPage is still visible in the path, remove it
+        if ($this->appConfig->indexPage !== '' && str_starts_with($path, $this->appConfig->indexPage)) {
+            $remainingPath = substr($path, strlen($this->appConfig->indexPage));
+            // Only remove if followed by '/' (route) or nothing (root)
+            if ($remainingPath === '' || str_starts_with($remainingPath, '/')) {
+                $path = ltrim($remainingPath, '/');
+            }
+        }
+
         // This section ensures that even on servers that require the URI to
         // contain the query string (Nginx) a correct URI is found, and also
         // fixes the QUERY_STRING Server var and $_GET array.
diff --git a/user_guide_src/source/changelogs/v4.6.2.rst b/user_guide_src/source/changelogs/v4.6.2.rst
index f39a0c46cf81..e9d7a65eb99d 100644
--- a/user_guide_src/source/changelogs/v4.6.2.rst
+++ b/user_guide_src/source/changelogs/v4.6.2.rst
@@ -41,6 +41,7 @@ Bugs Fixed
 - **Email:** Fixed a bug where ``Email::getHostname()`` failed to use ``$_SERVER['SERVER_ADDR']`` when ``$_SERVER['SERVER_NAME']`` was not set.
 - **Security:** Fixed a bug where the ``sanitize_filename()`` function from the Security helper would throw an error when used in CLI requests.
 - **Session:** Fixed a bug where using the ``DatabaseHandler`` with an unsupported database driver (such as ``SQLSRV``, ``OCI8``, or ``SQLite3``) did not throw an appropriate error.
+- **SiteURI:** Fixed a bug in ``SiteURIFactory::parseRequestURI()`` where serving the app from a subfolder using ``mod_rewrite`` while preserving the ``index.php`` file would cause incorrect route path detection.
 - **URI:** Fixed a bug in ``URI::getAuthority()`` where schemes without defined default ports (like ``rtsp://``) would cause issues due to missing array key handling.
 
 See the repo's


Test patch:
diff --git a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php
index d14a6e5e4151..fc717b78176f 100644
--- a/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php
+++ b/tests/system/HTTP/SiteURIFactoryDetectRoutePathTest.php
@@ -311,4 +311,97 @@ public static function provideExtensionPHP(): iterable
             ],
         ];
     }
+
+    #[DataProvider('provideRequestURIRewrite')]
+    public function testRequestURIRewrite(
+        string $requestUri,
+        string $scriptName,
+        string $indexPage,
+        string $expected,
+    ): void {
+        $server                = [];
+        $server['REQUEST_URI'] = $requestUri;
+        $server['SCRIPT_NAME'] = $scriptName;
+
+        $appConfig            = new App();
+        $appConfig->indexPage = $indexPage;
+
+        $factory = $this->createSiteURIFactory($server, $appConfig);
+
+        $this->assertSame($expected, $factory->detectRoutePath('REQUEST_URI'));
+    }
+
+    /**
+     * @return iterable<string, array{
+     *     requestUri: string,
+     *     scriptName: string,
+     *     indexPage: string,
+     *     expected: string
+     * }>
+     */
+    public static function provideRequestURIRewrite(): iterable
+    {
+        return [
+            'rewrite_with_route' => [
+                'requestUri' => '/ci/index.php/sample/method',
+                'scriptName' => '/ci/public/index.php',
+                'indexPage'  => 'index.php',
+                'expected'   => 'sample/method',
+            ],
+            'rewrite_root' => [
+                'requestUri' => '/ci/index.php',
+                'scriptName' => '/ci/public/index.php',
+                'indexPage'  => 'index.php',
+                'expected'   => '/',
+            ],
+            'rewrite_no_index_page' => [
+                'requestUri' => '/ci/sample/method',
+                'scriptName' => '/ci/public/index.php',
+                'indexPage'  => '',
+                'expected'   => 'sample/method',
+            ],
+            'rewrite_nested_subfolder' => [
+                'requestUri' => '/projects/index.php/api/users/list',
+                'scriptName' => '/projects/myapp/public/index.php',
+                'indexPage'  => 'index.php',
+                'expected'   => 'api/users/list',
+            ],
+            'rewrite_multiple_public_folders' => [
+                'requestUri' => '/public-sites/myapp/index.php/content/view',
+                'scriptName' => '/public-sites/myapp/public/index.php',
+                'indexPage'  => 'index.php',
+                'expected'   => 'content/view',
+            ],
+            'rewrite_custom_app_folder' => [
+                'requestUri' => '/myapp/index.php/products/category/electronics',
+                'scriptName' => '/myapp/web/index.php',
+                'indexPage'  => 'index.php',
+                'expected'   => 'products/category/electronics',
+            ],
+            'multiple_index_php_in_path' => [
+                'requestUri' => '/app/index.php/user/index.php/profile',
+                'scriptName' => '/app/public/index.php',
+                'indexPage'  => 'index.php',
+                'expected'   => 'user/index.php/profile',
+            ],
+            'custom_index_page_name' => [
+                'requestUri' => '/ci/app.php/users/list',
+                'scriptName' => '/ci/public/app.php',
+                'indexPage'  => 'app.php',
+                'expected'   => 'users/list',
+            ],
+            'custom_index_page_root' => [
+                'requestUri' => '/project/main.php',
+                'scriptName' => '/project/web/main.php',
+                'indexPage'  => 'main.php',
+                'expected'   => '/',
+            ],
+            'partial_match_should_not_remove' => [
+                'requestUri' => '/app/myindex.php/route',
+                'scriptName' => '/app/public/index.php',
+                'indexPage'  => 'index.php',
+                'expected'   => 'myindex.php/route',
+            ],
+        ];
+    }
 }