Repository: codeigniter4/CodeIgniter4 (PHP)

Issue:
Bug: base_url() removes trailing slash in baseURL
### PHP Version

8.1

### CodeIgniter4 Version

4.3.1

### CodeIgniter4 Installation Method

Git

### Which operating systems have you tested for this bug?

macOS

### Which server did you use?

cli-server (PHP built-in webserver)

### Database

n/a

### What happened?

`base_url()` returns `http://localhost:8080`.

The baseURL should have a trailing slash.
https://github.com/codeigniter4/CodeIgniter4/blob/ecef7f748df25a20f65bd2e2df26070fc3ebffdd/app/Config/App.php#L15-L16

### Steps to Reproduce

```php
<?php

namespace App\Controllers;

class Home extends BaseController
{
    public function index()
    {
        return base_url();
    }
}
```

### Expected Output

`http://localhost:8080/`

### Anything else?

CI 3.1.13 returns `http://localhost:8008/`.



Discussion:

Patch:
diff --git a/system/Helpers/url_helper.php b/system/Helpers/url_helper.php
index 1a3b78de5c91..5b0a0d6a690b 100644
--- a/system/Helpers/url_helper.php
+++ b/system/Helpers/url_helper.php
@@ -124,7 +124,7 @@ function base_url($relativePath = '', ?string $scheme = null): string
         $config            = clone config('App');
         $config->indexPage = '';
 
-        return rtrim(site_url($relativePath, $scheme, $config), '/');
+        return site_url($relativePath, $scheme, $config);
     }
 }
 
diff --git a/user_guide_src/source/changelogs/v4.3.2.rst b/user_guide_src/source/changelogs/v4.3.2.rst
index 9249f86fdb5d..fbb4e016ae63 100644
--- a/user_guide_src/source/changelogs/v4.3.2.rst
+++ b/user_guide_src/source/changelogs/v4.3.2.rst
@@ -12,6 +12,17 @@ Release Date: Unreleased
 BREAKING
 ********
 
+Behavior Changes
+================
+
+base_url()
+----------
+
+Due to a bug, in previous versions :php:func:`base_url()` without argument returned baseURL
+without a trailing slash (``/``) like ``http://localhost:8080``. Now it returns
+baseURL with a trailing slash. This is the same behavior as ``base_url()`` in
+CodeIgniter 3.
+
 Message Changes
 ***************
 
diff --git a/user_guide_src/source/helpers/url_helper.rst b/user_guide_src/source/helpers/url_helper.rst
index 1b17159bbe4a..389b83c3ef62 100644
--- a/user_guide_src/source/helpers/url_helper.rst
+++ b/user_guide_src/source/helpers/url_helper.rst
@@ -64,6 +64,10 @@ The following functions are available:
     .. note:: Since v4.3.0, if you set ``Config\App::$allowedHostnames``,
         this returns the URL with the hostname set in it if the current URL matches.
 
+    .. note:: In previous versions, this returned the base URL without a trailing
+        slash (``/``) when called with no argument. The bug was fixed and
+        since v4.3.2 it returns the base URL with a trailing slash.
+
     Returns your site base URL, as specified in your config file. Example:
 
     .. literalinclude:: url_helper/003.php
diff --git a/user_guide_src/source/installation/upgrade_432.rst b/user_guide_src/source/installation/upgrade_432.rst
new file mode 100644
index 000000000000..f61e57012d6a
--- /dev/null
+++ b/user_guide_src/source/installation/upgrade_432.rst
@@ -0,0 +1,56 @@
+##############################
+Upgrading from 4.3.1 to 4.3.2
+##############################
+
+Please refer to the upgrade instructions corresponding to your installation method.
+
+- :ref:`Composer Installation App Starter Upgrading <app-starter-upgrading>`
+- :ref:`Composer Installation Adding CodeIgniter4 to an Existing Project Upgrading <adding-codeigniter4-upgrading>`
+- :ref:`Manual Installation Upgrading <installing-manual-upgrading>`
+
+.. contents::
+    :local:
+    :depth: 2
+
+Breaking Changes
+****************
+
+base_url()
+==========
+
+The :php:func:`base_url()` behavior has been fix. In previous versions, when you
+call ``base_url()`` **without argument**, it returned baseURL without a trailing
+slash (``/``). Now it returns baseURL with a trailing slash. For example:
+
+- before: ``http://example.com``
+- after: ``http://example.com/``
+
+If you have code to call ``base_url()`` without argument, you may need to adjust the URLs.
+
+Project Files
+*************
+
+Some files in the **project space** (root, app, public, writable) received updates. Due to
+these files being outside of the **system** scope they will not be changed without your intervention.
+
+There are some third-party CodeIgniter modules available to assist with merging changes to
+the project space: `Explore on Packagist <https://packagist.org/explore/?query=codeigniter4%20updates>`_.
+
+Content Changes
+===============
+
+The following files received significant changes (including deprecations or visual adjustments)
+and it is recommended that you merge the updated versions with your application:
+
+Config
+------
+
+- @TODO
+
+All Changes
+===========
+
+This is a list of all files in the **project space** that received changes;
+many will be simple comments or formatting that have no effect on the runtime:
+
+- @TODO
diff --git a/user_guide_src/source/installation/upgrading.rst b/user_guide_src/source/installation/upgrading.rst
index f5600aa52d00..d754d020b55c 100644
--- a/user_guide_src/source/installation/upgrading.rst
+++ b/user_guide_src/source/installation/upgrading.rst
@@ -16,6 +16,7 @@ See also :doc:`./backward_compatibility_notes`.
 
     backward_compatibility_notes
 
+    upgrade_432
     upgrade_431
     upgrade_430
     upgrade_4212


Test patch:
diff --git a/tests/system/Helpers/URLHelper/SiteUrlTest.php b/tests/system/Helpers/URLHelper/SiteUrlTest.php
index 1f6f081b4dc2..9873292843e5 100644
--- a/tests/system/Helpers/URLHelper/SiteUrlTest.php
+++ b/tests/system/Helpers/URLHelper/SiteUrlTest.php
@@ -59,27 +59,31 @@ protected function tearDown(): void
      * @param bool        $secure
      * @param string      $path
      * @param string      $expectedSiteUrl
+     * @param string      $expectedBaseUrl
      *
      * @dataProvider configProvider
      */
-    public function testUrls($baseURL, $indexPage, $scheme, $secure, $path, $expectedSiteUrl)
-    {
+    public function testUrls(
+        $baseURL,
+        $indexPage,
+        $scheme,
+        $secure,
+        $path,
+        $expectedSiteUrl,
+        $expectedBaseUrl
+    ) {
         // Set the config
         $this->config->baseURL                   = $baseURL;
         $this->config->indexPage                 = $indexPage;
         $this->config->forceGlobalSecureRequests = $secure;
 
         $this->assertSame($expectedSiteUrl, site_url($path, $scheme, $this->config));
-
-        // base_url is always the trimmed site_url without index page
-        $expectedBaseUrl = $indexPage === '' ? $expectedSiteUrl : str_replace('/' . $indexPage, '', $expectedSiteUrl);
-        $expectedBaseUrl = rtrim($expectedBaseUrl, '/');
         $this->assertSame($expectedBaseUrl, base_url($path, $scheme));
     }
 
     public function configProvider()
     {
-        // baseURL, indexPage, scheme, secure, path, expectedSiteUrl
+        // baseURL, indexPage, scheme, secure, path, expectedSiteUrl, expectedBaseUrl
         return [
             'forceGlobalSecure' => [
                 'http://example.com/',
@@ -88,6 +92,7 @@ public function configProvider()
                 true,
                 '',
                 'https://example.com/index.php',
+                'https://example.com/',
             ],
             [
                 'http://example.com/',
@@ -96,14 +101,16 @@ public function configProvider()
                 false,
                 '',
                 'http://example.com/index.php',
+                'http://example.com/',
             ],
-            [
+            'baseURL missing /' => [
                 'http://example.com',
                 'index.php',
                 null,
                 false,
                 '',
                 'http://example.com/index.php',
+                'http://example.com/',
             ],
             [
                 'http://example.com/',
@@ -112,6 +119,7 @@ public function configProvider()
                 false,
                 '',
                 'http://example.com/',
+                'http://example.com/',
             ],
             [
                 'http://example.com/',
@@ -120,6 +128,7 @@ public function configProvider()
                 false,
                 '',
                 'http://example.com/banana.php',
+                'http://example.com/',
             ],
             [
                 'http://example.com/',
@@ -128,6 +137,7 @@ public function configProvider()
                 false,
                 'abc',
                 'http://example.com/abc',
+                'http://example.com/abc',
             ],
             'URL decode' => [
                 'http://example.com/',
@@ -136,6 +146,7 @@ public function configProvider()
                 false,
                 'template/meet-%26-greet',
                 'http://example.com/template/meet-&-greet',
+                'http://example.com/template/meet-&-greet',
             ],
             'URL encode' => [
                 'http://example.com/',
@@ -144,6 +155,7 @@ public function configProvider()
                 false,
                 '
alert
',
                 'http://example.com/%3Cs%3Ealert%3C/s%3E',
+                'http://example.com/%3Cs%3Ealert%3C/s%3E',
             ],
             [
                 'http://example.com/public/',
@@ -152,6 +164,7 @@ public function configProvider()
                 false,
                 '',
                 'http://example.com/public/index.php',
+                'http://example.com/public/',
             ],
             [
                 'http://example.com/public/',
@@ -160,6 +173,7 @@ public function configProvider()
                 false,
                 '',
                 'http://example.com/public/',
+                'http://example.com/public/',
             ],
             [
                 'http://example.com/public',
@@ -168,6 +182,7 @@ public function configProvider()
                 false,
                 '',
                 'http://example.com/public/',
+                'http://example.com/public/',
             ],
             [
                 'http://example.com/public',
@@ -176,6 +191,7 @@ public function configProvider()
                 false,
                 '/',
                 'http://example.com/public/index.php/',
+                'http://example.com/public/',
             ],
             [
                 'http://example.com/public/',
@@ -184,6 +200,7 @@ public function configProvider()
                 false,
                 '/',
                 'http://example.com/public/index.php/',
+                'http://example.com/public/',
             ],
             [
                 'http://example.com/',
@@ -192,6 +209,7 @@ public function configProvider()
                 false,
                 'foo',
                 'http://example.com/index.php/foo',
+                'http://example.com/foo',
             ],
             [
                 'http://example.com/',
@@ -200,6 +218,7 @@ public function configProvider()
                 false,
                 '0',
                 'http://example.com/index.php/0',
+                'http://example.com/0',
             ],
             [
                 'http://example.com/public',
@@ -208,6 +227,7 @@ public function configProvider()
                 false,
                 'foo',
                 'http://example.com/public/index.php/foo',
+                'http://example.com/public/foo',
             ],
             [
                 'http://example.com/',
@@ -216,6 +236,7 @@ public function configProvider()
                 false,
                 'foo?bar=bam',
                 'http://example.com/index.php/foo?bar=bam',
+                'http://example.com/foo?bar=bam',
             ],
             [
                 'http://example.com/',
@@ -224,6 +245,7 @@ public function configProvider()
                 false,
                 'test#banana',
                 'http://example.com/index.php/test#banana',
+                'http://example.com/test#banana',
             ],
             [
                 'http://example.com/',
@@ -232,6 +254,7 @@ public function configProvider()
                 false,
                 'foo',
                 'ftp://example.com/index.php/foo',
+                'ftp://example.com/foo',
             ],
             [
                 'http://example.com/',
@@ -240,6 +263,7 @@ public function configProvider()
                 false,
                 'news/local/123',
                 'http://example.com/index.php/news/local/123',
+                'http://example.com/news/local/123',
             ],
             [
                 'http://example.com/',
@@ -248,6 +272,7 @@ public function configProvider()
                 false,
                 ['news', 'local', '123'],
                 'http://example.com/index.php/news/local/123',
+                'http://example.com/news/local/123',
             ],
         ];
     }
@@ -267,12 +292,12 @@ public function testBaseURLDiscovery()
         $_SERVER['HTTP_HOST']   = 'example.com';
         $_SERVER['REQUEST_URI'] = '/test';
 
-        $this->assertSame('http://example.com', base_url());
+        $this->assertSame('http://example.com/', base_url());
 
         $_SERVER['HTTP_HOST']   = 'example.com';
         $_SERVER['REQUEST_URI'] = '/test/page';
 
-        $this->assertSame('http://example.com', base_url());
+        $this->assertSame('http://example.com/', base_url());
         $this->assertSame('http://example.com/profile', base_url('profile'));
     }