]> git.immae.eu Git - github/shaarli/Shaarli.git/commitdiff
Merge pull request #1561 from ArthurHoaro/feature/front-deps-upgrade
authorArthurHoaro <arthur@hoa.ro>
Wed, 23 Sep 2020 13:50:23 +0000 (15:50 +0200)
committerGitHub <noreply@github.com>
Wed, 23 Sep 2020 13:50:23 +0000 (15:50 +0200)
16 files changed:
.htaccess
application/feed/FeedBuilder.php
application/front/controller/admin/ManageShaareController.php
application/front/controller/visitor/DailyController.php
application/http/HttpUtils.php
doc/md/Shaarli-configuration.md
index.php
tests/feed/FeedBuilderTest.php
tests/front/controller/admin/ExportControllerTest.php
tests/front/controller/admin/ToolsControllerTest.php
tests/front/controller/visitor/DailyControllerTest.php
tests/front/controller/visitor/FrontControllerMockHelper.php
tests/front/controller/visitor/InstallControllerTest.php
tests/front/controller/visitor/OpenSearchControllerTest.php
tests/http/HttpUtils/IndexUrlTest.php
tests/http/HttpUtils/IndexUrlTestWithConstant.php [new file with mode: 0644]

index 8876e346ce70bf3ff951a5ed3e2fe682de837535..af2dc5a7ff24a244d731a5d2382dc496f62d1eda 100644 (file)
--- a/.htaccess
+++ b/.htaccess
@@ -16,23 +16,7 @@ RewriteCond %{REQUEST_FILENAME} !-f
 RewriteCond %{REQUEST_FILENAME} !-d
 RewriteRule ^ index.php [QSA,L]
 
-<Limit GET POST PUT DELETE OPTIONS>
-  <IfModule version_module>
-    <IfVersion >= 2.4>
-       Require all granted
-    </IfVersion>
-    <IfVersion < 2.4>
-       Allow from all
-       Deny from none
-    </IfVersion>
-  </IfModule>
-
-  <IfModule !version_module>
-    Require all granted
-  </IfModule>
-</Limit>
-
-<LimitExcept GET POST PUT DELETE OPTIONS>
+<LimitExcept GET POST PUT DELETE PATCH OPTIONS>
   <IfModule version_module>
     <IfVersion >= 2.4>
        Require all denied
index 3653c32f981b360d52d556e4ea48e5ee6c1e4ab3..f6def6308849eb6401fd908a4d063046f163dc11 100644 (file)
@@ -122,9 +122,9 @@ class FeedBuilder
         $data['language'] = $this->getTypeLanguage($feedType);
         $data['last_update'] = $this->getLatestDateFormatted($feedType);
         $data['show_dates'] = !$this->hideDates || $this->isLoggedIn;
-        // Remove leading slash from REQUEST_URI.
-        $data['self_link'] = escape(server_url($this->serverInfo))
-            . escape($this->serverInfo['REQUEST_URI']);
+        // Remove leading path from REQUEST_URI (already contained in $pageaddr).
+        $requestUri = preg_replace('#(.*?/)(feed.*)#', '$2', escape($this->serverInfo['REQUEST_URI']));
+        $data['self_link'] = $pageaddr . $requestUri;
         $data['index_url'] = $pageaddr;
         $data['usepermalinks'] = $this->usePermalinks === true;
         $data['links'] = $linkDisplayed;
index 33e1188ef73a6f0df55c2daba25e88cea29d8f91..ca2da9b53b7edb4e545ba7b337f04138313ab3f3 100644 (file)
@@ -169,7 +169,7 @@ class ManageShaareController extends ShaarliAdminController
         return $this->redirectFromReferer(
             $request,
             $response,
-            ['add-shaare', 'shaare'], ['addlink', 'post', 'edit_link'],
+            ['/admin/add-shaare', '/admin/shaare'], ['addlink', 'post', 'edit_link'],
             $bookmark->getShortUrl()
         );
     }
index 54a4778fa3280d7b75829ed01a41bd42253cba89..07617cf11fdfe49b047c24d0641477ad6b66e8d4 100644 (file)
@@ -132,7 +132,7 @@ class DailyController extends ShaarliVisitorController
                 'date' => $dayDatetime,
                 'date_rss' => $dayDatetime->format(DateTime::RSS),
                 'date_human' => format_date($dayDatetime, false, true),
-                'absolute_url' => $indexUrl . '/daily?day=' . $day,
+                'absolute_url' => $indexUrl . 'daily?day=' . $day,
                 'links' => [],
             ];
 
index 4fc4e3dcff08f3457c7a2d541962c699979732c7..9f4140735a695c4ab8e08b2c10a8e49eaa3527bb 100644 (file)
@@ -369,7 +369,11 @@ function server_url($server)
  */
 function index_url($server)
 {
-    $scriptname = $server['SCRIPT_NAME'] ?? '';
+    if (defined('SHAARLI_ROOT_URL') && null !== SHAARLI_ROOT_URL) {
+        return rtrim(SHAARLI_ROOT_URL, '/') . '/';
+    }
+
+    $scriptname = !empty($server['SCRIPT_NAME']) ? $server['SCRIPT_NAME'] : '/';
     if (endsWith($scriptname, 'index.php')) {
         $scriptname = substr($scriptname, 0, -9);
     }
@@ -392,7 +396,7 @@ function page_url($server)
         $scriptname = substr($scriptname, 0, -9);
     }
 
-    $route = ltrim($server['REQUEST_URI'] ?? '', $scriptname);
+    $route = preg_replace('@^' . $scriptname . '@', '', $server['REQUEST_URI'] ?? '');
     if (! empty($server['QUERY_STRING'])) {
         return index_url($server) . $route . '?' . $server['QUERY_STRING'];
     }
index 14eec7b2f4ab7129d5f53af2c1b29119f09794ba..263fb7616014ebba6e2a4384d9e1741c21a93106 100644 (file)
@@ -7,7 +7,7 @@ Once your Shaarli instance is installed, the file `data/config.json.php` is gene
 - its values override those defined in `index.php`
 - it is wrapped in a PHP comment so that its contents are never served by the web server, regardless of configuration
 
-**Do not edit configuration options in index.php! Your changes would be lost.** 
+**Do not edit configuration options in index.php! Your changes would be lost.**
 
 ## Tools menu
 
@@ -135,71 +135,72 @@ Some settings can be configured directly from a web browser by accesing the `Too
 ## Settings
 
 ### Credentials
+
 _These settings should not be edited_
 
-- **login**: Login username.  
-- **hash**: Generated password hash.  
+- **login**: Login username.
+- **hash**: Generated password hash.
 - **salt**: Password salt.
 
 ### General
 
-- **title**: Shaarli's instance title.  
-- **header_link**: Link to the homepage.  
-- **links_per_page**: Number of Shaares displayed per page.  
-- **timezone**: See [the list of supported timezones](http://php.net/manual/en/timezones.php).  
+- **title**: Shaarli's instance title.
+- **header_link**: Link to the homepage.
+- **links_per_page**: Number of Shaares displayed per page.
+- **timezone**: See [the list of supported timezones](http://php.net/manual/en/timezones.php).
 - **enabled_plugins**: List of enabled plugins.
 - **default_note_title**: Default title of a new note.
 - **retrieve_description** (boolean): If set to true, for every new Shaare Shaarli will try to retrieve the description and keywords from the HTML meta tags.
+- **root_url**: Overrides automatic discovery of Shaarli instance's URL (e.g.) `https://sub.domain.tld/shaarli-folder/`.
 
 ### Security
 
-- **session_protection_disabled**: Disable session cookie hijacking protection (not recommended). 
-  It might be useful if your IP adress often changes.  
-- **ban_after**: Failed login attempts before being IP banned.  
-- **ban_duration**: IP ban duration in seconds.  
-- **open_shaarli**: Anyone can add a new Shaare while logged out if enabled.  
-- **trusted_proxies**: List of trusted IP which won't be banned after failed login attemps. Useful if Shaarli is behind a reverse proxy.  
+- **session_protection_disabled**: Disable session cookie hijacking protection (not recommended).
+  It might be useful if your IP adress often changes.
+- **ban_after**: Failed login attempts before being IP banned.
+- **ban_duration**: IP ban duration in seconds.
+- **open_shaarli**: Anyone can add a new Shaare while logged out if enabled.
+- **trusted_proxies**: List of trusted IP which won't be banned after failed login attemps. Useful if Shaarli is behind a reverse proxy.
 - **allowed_protocols**: List of allowed protocols in shaare URLs or markdown-rendered descriptions. Useful if you want to store `javascript:` links (bookmarklets) in Shaarli (default: `["ftp", "ftps", "magnet"]`).
 
 ### Resources
 
-- **data_dir**: Data directory.  
-- **datastore**: Shaarli's Shaares database file path.  
+- **data_dir**: Data directory.
+- **datastore**: Shaarli's Shaares database file path.
 - **history**: Shaarli's operation history file path.
-- **updates**: File path for the ran updates file.  
-- **log**: Log file path.  
-- **update_check**: Last update check file path.  
-- **raintpl_tpl**: Templates directory.  
-- **raintpl_tmp**: Template engine cache directory.  
-- **thumbnails_cache**: Thumbnails cache directory.  
-- **page_cache**: Shaarli's internal cache directory.  
+- **updates**: File path for the ran updates file.
+- **log**: Log file path.
+- **update_check**: Last update check file path.
+- **raintpl_tpl**: Templates directory.
+- **raintpl_tmp**: Template engine cache directory.
+- **thumbnails_cache**: Thumbnails cache directory.
+- **page_cache**: Shaarli's internal cache directory.
 - **ban_file**: Banned IP file path.
 
 ### Translation
 
 - **language**: translation language (also see [Translations](Translations))
-    - **auto** (default): The translation language is chosen from the browser locale. 
-    It means that the language can be different for 2 different visitors depending on their locale.  
+    - **auto** (default): The translation language is chosen from the browser locale.
+    It means that the language can be different for 2 different visitors depending on their locale.
     - **en**: Use the English translation.
     - **fr**: Use the French translation.
-- **mode**: 
+- **mode**:
     - **auto** or **php** (default): Use the PHP implementation of gettext (slower)
-    - **gettext**: Use PHP builtin gettext extension 
+    - **gettext**: Use PHP builtin gettext extension
     (faster, but requires `php-gettext` to be installed and to reload the web server on update)
-- **extension**: Translation extensions for custom themes or plugins. 
+- **extension**: Translation extensions for custom themes or plugins.
 Must be an associative array: `translation domain => translation path`.
 
 ### Updates
 
-- **check_updates**: Enable or disable update check to the git repository.  
-- **check_updates_branch**: Git branch used to check updates (e.g. `stable` or `master`).  
+- **check_updates**: Enable or disable update check to the git repository.
+- **check_updates_branch**: Git branch used to check updates (e.g. `stable` or `master`).
 - **check_updates_interval**: Look for new version every N seconds (default: every day).
 
 ### Privacy
 
-- **default_private_links**: Check the private checkbox by default for every new Shaare.  
-- **hide_public_links**: All Shaares are hidden while logged out.  
+- **default_private_links**: Check the private checkbox by default for every new Shaare.
+- **hide_public_links**: All Shaares are hidden while logged out.
 - **force_login**: if **hide_public_links** and this are set to `true`, all anonymous users are redirected to the login page.
 - **hide_timestamps**: Timestamps are hidden.
 - **remember_user_default**: Default state of the login page's *remember me* checkbox
@@ -207,14 +208,14 @@ Must be an associative array: `translation domain => translation path`.
 
 ### Feed
 
-- **rss_permalinks**: Enable this to redirect RSS links to Shaarli's permalinks instead of shaared URL.  
+- **rss_permalinks**: Enable this to redirect RSS links to Shaarli's permalinks instead of shaared URL.
 - **show_atom**: Display ATOM feed button.
 
 ### Thumbnail
 
-- **enable_thumbnails**: Enable or disable thumbnail display.  
+- **enable_thumbnails**: Enable or disable thumbnail display.
 - **enable_localcache**: Enable or disable local cache.
 
 ## Plugins configuration
 
-See [Plugins](Plugins.md)
\ No newline at end of file
+See [Plugins](Plugins.md)
index 869f42de1352eafb5ec95bf44de6ca2c1b29855e..b10397dda2d079cb785caa6fddef262d77331503 100644 (file)
--- a/index.php
+++ b/index.php
@@ -35,6 +35,9 @@ use Slim\App;
 
 $conf = new ConfigManager();
 
+// Manually override root URL for complex server configurations
+define('SHAARLI_ROOT_URL', $conf->get('general.root_url', null));
+
 // In dev mode, throw exception on any warning
 if ($conf->get('dev.debug', false)) {
     // See all errors (for debugging only)
index fe37d5f23a08068ddb2e8e91f77c872225173deb..5dfe73aa49ce9bf047ecc2833ed8bc9375e85c3b 100644 (file)
@@ -3,6 +3,7 @@
 namespace Shaarli\Feed;
 
 use DateTime;
+use PHPUnit\Framework\TestCase;
 use ReferenceLinkDB;
 use Shaarli\Bookmark\Bookmark;
 use Shaarli\Bookmark\BookmarkFileService;
@@ -16,7 +17,7 @@ use Shaarli\History;
  *
  * Unit tests for FeedBuilder.
  */
-class FeedBuilderTest extends \PHPUnit\Framework\TestCase
+class FeedBuilderTest extends TestCase
 {
     /**
      * @var string locale Basque (Spain).
@@ -44,7 +45,7 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
     /**
      * Called before every test method.
      */
-    public static function setUpBeforeClass()
+    public static function setUpBeforeClass(): void
     {
         $conf = new ConfigManager('tests/utils/config/configJson');
         $conf->set('resource.datastore', self::$testDatastore);
@@ -60,7 +61,7 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
             'SERVER_NAME' => 'host.tld',
             'SERVER_PORT' => '80',
             'SCRIPT_NAME' => '/index.php',
-            'REQUEST_URI' => '/index.php?do=feed',
+            'REQUEST_URI' => '/feed/atom',
         );
     }
 
@@ -81,7 +82,7 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
         $this->assertEquals(self::$RSS_LANGUAGE, $data['language']);
         $this->assertRegExp('/Wed, 03 Aug 2016 09:30:33 \+\d{4}/', $data['last_update']);
         $this->assertEquals(true, $data['show_dates']);
-        $this->assertEquals('http://host.tld/index.php?do=feed', $data['self_link']);
+        $this->assertEquals('http://host.tld/feed/atom', $data['self_link']);
         $this->assertEquals('http://host.tld/', $data['index_url']);
         $this->assertFalse($data['usepermalinks']);
         $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
@@ -253,7 +254,7 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
             'SERVER_NAME' => 'host.tld',
             'SERVER_PORT' => '8080',
             'SCRIPT_NAME' => '/~user/shaarli/index.php',
-            'REQUEST_URI' => '/~user/shaarli/index.php?do=feed',
+            'REQUEST_URI' => '/~user/shaarli/feed/atom',
         );
         $feedBuilder = new FeedBuilder(
             self::$bookmarkService,
@@ -265,7 +266,7 @@ class FeedBuilderTest extends \PHPUnit\Framework\TestCase
         $data = $feedBuilder->buildData(FeedBuilder::$FEED_ATOM, null);
 
         $this->assertEquals(
-            'http://host.tld:8080/~user/shaarli/index.php?do=feed',
+            'http://host.tld:8080/~user/shaarli/feed/atom',
             $data['self_link']
         );
 
index 50d9e378752f890024c8ee59e89ebd1f3120be4a..12d26f4a5ff5e89b93cc8514691a94b034fc093c 100644 (file)
@@ -84,7 +84,7 @@ class ExportControllerTest extends TestCase
                     static::assertInstanceOf(BookmarkRawFormatter::class, $formatter);
                     static::assertSame($parameters['selection'], $selection);
                     static::assertTrue($prependNoteUrl);
-                    static::assertSame('http://shaarli', $indexUrl);
+                    static::assertSame('http://shaarli/subfolder/', $indexUrl);
 
                     return $bookmarks;
                 }
index fc756f0ffcf75889285014460ccee28722c030bc..39144d2f0dca0960098db18e1c241f050d5f73b5 100644 (file)
@@ -8,7 +8,7 @@ use PHPUnit\Framework\TestCase;
 use Slim\Http\Request;
 use Slim\Http\Response;
 
-class ToolsControllerTestControllerTest extends TestCase
+class ToolsControllerTest extends TestCase
 {
     use FrontAdminControllerMockHelper;
 
@@ -41,7 +41,7 @@ class ToolsControllerTestControllerTest extends TestCase
 
         static::assertSame(200, $result->getStatusCode());
         static::assertSame('tools', (string) $result->getBody());
-        static::assertSame('https://shaarli', $assignedVariables['pageabsaddr']);
+        static::assertSame('https://shaarli/', $assignedVariables['pageabsaddr']);
         static::assertTrue($assignedVariables['sslenabled']);
     }
 
@@ -63,7 +63,7 @@ class ToolsControllerTestControllerTest extends TestCase
 
         static::assertSame(200, $result->getStatusCode());
         static::assertSame('tools', (string) $result->getBody());
-        static::assertSame('http://shaarli', $assignedVariables['pageabsaddr']);
+        static::assertSame('http://shaarli/', $assignedVariables['pageabsaddr']);
         static::assertFalse($assignedVariables['sslenabled']);
     }
 }
index b802c62c53da3c4a5129dce75168dec552df22d8..cb5b96f3b557174d5d9bb6f15914f362790861bf 100644 (file)
@@ -392,8 +392,8 @@ class DailyControllerTest extends TestCase
         static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
         static::assertSame('dailyrss', (string) $result->getBody());
         static::assertSame('Shaarli', $assignedVariables['title']);
-        static::assertSame('http://shaarli', $assignedVariables['index_url']);
-        static::assertSame('http://shaarli/daily-rss', $assignedVariables['page_url']);
+        static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']);
+        static::assertSame('http://shaarli/subfolder/daily-rss', $assignedVariables['page_url']);
         static::assertFalse($assignedVariables['hide_timestamps']);
         static::assertCount(2, $assignedVariables['days']);
 
@@ -402,7 +402,7 @@ class DailyControllerTest extends TestCase
         static::assertEquals($dates[0], $day['date']);
         static::assertSame($dates[0]->format(\DateTime::RSS), $day['date_rss']);
         static::assertSame(format_date($dates[0], false), $day['date_human']);
-        static::assertSame('http://shaarli/daily?day='. $dates[0]->format('Ymd'), $day['absolute_url']);
+        static::assertSame('http://shaarli/subfolder/daily?day='. $dates[0]->format('Ymd'), $day['absolute_url']);
         static::assertCount(1, $day['links']);
         static::assertSame(1, $day['links'][0]['id']);
         static::assertSame('http://domain.tld/1', $day['links'][0]['url']);
@@ -413,7 +413,7 @@ class DailyControllerTest extends TestCase
         static::assertEquals($dates[1], $day['date']);
         static::assertSame($dates[1]->format(\DateTime::RSS), $day['date_rss']);
         static::assertSame(format_date($dates[1], false), $day['date_human']);
-        static::assertSame('http://shaarli/daily?day='. $dates[1]->format('Ymd'), $day['absolute_url']);
+        static::assertSame('http://shaarli/subfolder/daily?day='. $dates[1]->format('Ymd'), $day['absolute_url']);
         static::assertCount(2, $day['links']);
 
         static::assertSame(2, $day['links'][0]['id']);
@@ -468,8 +468,8 @@ class DailyControllerTest extends TestCase
         static::assertStringContainsString('application/rss', $result->getHeader('Content-Type')[0]);
         static::assertSame('dailyrss', (string) $result->getBody());
         static::assertSame('Shaarli', $assignedVariables['title']);
-        static::assertSame('http://shaarli', $assignedVariables['index_url']);
-        static::assertSame('http://shaarli/daily-rss', $assignedVariables['page_url']);
+        static::assertSame('http://shaarli/subfolder/', $assignedVariables['index_url']);
+        static::assertSame('http://shaarli/subfolder/daily-rss', $assignedVariables['page_url']);
         static::assertFalse($assignedVariables['hide_timestamps']);
         static::assertCount(0, $assignedVariables['days']);
     }
index 927e7f0a15eef7acda822afc5ef1eaf6322e3571..6c53289bd01c83ba018003e2bf4dc32a27da1139 100644 (file)
@@ -79,8 +79,9 @@ trait FrontControllerMockHelper
         $this->container->environment = [
             'SERVER_NAME' => 'shaarli',
             'SERVER_PORT' => '80',
-            'REQUEST_URI' => '/daily-rss',
+            'REQUEST_URI' => '/subfolder/daily-rss',
             'REMOTE_ADDR' => '1.2.3.4',
+            'SCRIPT_NAME' => '/subfolder/index.php',
         ];
 
         $this->container->basePath = '/subfolder';
index 3b855365024a9e9e8462299ed5e1e85a74449aaf..994d3f33fb87aa01d65accfa23fdbe50e235ccf0 100644 (file)
@@ -257,6 +257,39 @@ class InstallControllerTest extends TestCase
         static::assertSame('/subfolder/login', $result->getHeader('location')[0]);
 
         static::assertSame('UTC', $confSettings['general.timezone']);
-        static::assertSame('Shared bookmarks on http://shaarli', $confSettings['general.title']);
+        static::assertSame('Shared bookmarks on http://shaarli/subfolder/', $confSettings['general.title']);
+    }
+
+    /**
+     * Same test  as testSaveInstallDefaultValues() but for an instance install in root directory.
+     */
+    public function testSaveInstallDefaultValuesWithoutSubfolder(): void
+    {
+        $confSettings = [];
+
+        $this->container->environment = [
+            'SERVER_NAME' => 'shaarli',
+            'SERVER_PORT' => '80',
+            'REQUEST_URI' => '/install',
+            'REMOTE_ADDR' => '1.2.3.4',
+            'SCRIPT_NAME' => '/index.php',
+        ];
+
+        $this->container->basePath = '';
+
+        $request = $this->createMock(Request::class);
+        $response = new Response();
+
+        $this->container->conf->method('set')->willReturnCallback(function (string $key, $value) use (&$confSettings) {
+            $confSettings[$key] = $value;
+        });
+
+        $result = $this->controller->save($request, $response);
+
+        static::assertSame(302, $result->getStatusCode());
+        static::assertSame('/login', $result->getHeader('location')[0]);
+
+        static::assertSame('UTC', $confSettings['general.timezone']);
+        static::assertSame('Shared bookmarks on http://shaarli/', $confSettings['general.title']);
     }
 }
index 5f9f5b1285c42eb72200710a24d8f2fa26c0cd7e..9609a377f71d638de004f00716010e019aef0ed9 100644 (file)
@@ -39,6 +39,6 @@ class OpenSearchControllerTest extends TestCase
             $result->getHeader('Content-Type')[0]
         );
         static::assertSame('opensearch', (string) $result->getBody());
-        static::assertSame('http://shaarli', $assignedVariables['serverurl']);
+        static::assertSame('http://shaarli/subfolder/', $assignedVariables['serverurl']);
     }
 }
index 73d33cd450d783237829c133641783b5715243b3..cce45c51e18dd0b4b2426ffff61d3cbd94d49ffa 100644 (file)
@@ -5,12 +5,14 @@
 
 namespace Shaarli\Http;
 
+use PHPUnit\Framework\TestCase;
+
 require_once 'application/http/HttpUtils.php';
 
 /**
  * Unitary tests for index_url()
  */
-class IndexUrlTest extends \PHPUnit\Framework\TestCase
+class IndexUrlTest extends TestCase
 {
     /**
      * If on the main page, remove "index.php" from the URL resource
@@ -103,4 +105,36 @@ class IndexUrlTest extends \PHPUnit\Framework\TestCase
             )
         );
     }
+
+    /**
+     * The route is stored in REQUEST_URI and subfolder
+     */
+    public function testPageUrlWithRouteUnderSubfolder()
+    {
+        $this->assertEquals(
+            'http://host.tld/subfolder/picture-wall',
+            page_url(
+                array(
+                    'HTTPS' => 'Off',
+                    'SERVER_NAME' => 'host.tld',
+                    'SERVER_PORT' => '80',
+                    'SCRIPT_NAME' => '/subfolder/index.php',
+                    'REQUEST_URI' => '/subfolder/picture-wall',
+                )
+            )
+        );
+
+        $this->assertEquals(
+            'http://host.tld/subfolder/admin/picture-wall',
+            page_url(
+                array(
+                    'HTTPS' => 'Off',
+                    'SERVER_NAME' => 'host.tld',
+                    'SERVER_PORT' => '80',
+                    'SCRIPT_NAME' => '/subfolder/admin/index.php',
+                    'REQUEST_URI' => '/subfolder/admin/picture-wall',
+                )
+            )
+        );
+    }
 }
diff --git a/tests/http/HttpUtils/IndexUrlTestWithConstant.php b/tests/http/HttpUtils/IndexUrlTestWithConstant.php
new file mode 100644 (file)
index 0000000..15ca3d7
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Shaarli\Http;
+
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Test index_url with SHAARLI_ROOT_URL defined to override automatic retrieval.
+ * This should stay in its dedicated class to make sure to not alter other tests of the suite.
+ */
+class IndexUrlTestWithConstant extends TestCase
+{
+    public static function setUpBeforeClass(): void
+    {
+        define('SHAARLI_ROOT_URL', 'http://other-host.tld/subfolder/');
+    }
+
+    /**
+     * The route is stored in REQUEST_URI and subfolder
+     */
+    public function testIndexUrlWithConstantDefined()
+    {
+        $this->assertEquals(
+            'http://other-host.tld/subfolder/',
+            index_url(
+                array(
+                    'HTTPS' => 'Off',
+                    'SERVER_NAME' => 'host.tld',
+                    'SERVER_PORT' => '80',
+                    'SCRIPT_NAME' => '/index.php',
+                    'REQUEST_URI' => '/picture-wall',
+                )
+            )
+        );
+
+        $this->assertEquals(
+            'http://other-host.tld/subfolder/',
+            index_url(
+                array(
+                    'HTTPS' => 'Off',
+                    'SERVER_NAME' => 'host.tld',
+                    'SERVER_PORT' => '80',
+                    'SCRIPT_NAME' => '/admin/index.php',
+                    'REQUEST_URI' => '/admin/picture-wall',
+                )
+            )
+        );
+    }
+}