diff --git a/application/feed/FeedBuilder.php b/application/feed/FeedBuilder.php index 3653c32f..f6def630 100644 --- a/application/feed/FeedBuilder.php +++ b/application/feed/FeedBuilder.php @@ -122,9 +122,9 @@ public function buildData(string $feedType, ?array $userInput) $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; diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php index 54a4778f..07617cf1 100644 --- a/application/front/controller/visitor/DailyController.php +++ b/application/front/controller/visitor/DailyController.php @@ -132,7 +132,7 @@ public function rss(Request $request, Response $response): Response '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' => [], ]; diff --git a/application/http/HttpUtils.php b/application/http/HttpUtils.php index 4fc4e3dc..9f414073 100644 --- a/application/http/HttpUtils.php +++ b/application/http/HttpUtils.php @@ -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']; } diff --git a/doc/md/Shaarli-configuration.md b/doc/md/Shaarli-configuration.md index 14eec7b2..263fb761 100644 --- a/doc/md/Shaarli-configuration.md +++ b/doc/md/Shaarli-configuration.md @@ -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) diff --git a/index.php b/index.php index 869f42de..b10397dd 100644 --- a/index.php +++ b/index.php @@ -35,6 +35,9 @@ $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) diff --git a/tests/feed/FeedBuilderTest.php b/tests/feed/FeedBuilderTest.php index fe37d5f2..5dfe73aa 100644 --- a/tests/feed/FeedBuilderTest.php +++ b/tests/feed/FeedBuilderTest.php @@ -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 @@ * * 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 @@ public static function setUpBeforeClass() '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 @@ public function testRSSBuildData() $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 @@ public function testBuildDataServerSubdir() '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 @@ public function testBuildDataServerSubdir() $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'] ); diff --git a/tests/front/controller/admin/ExportControllerTest.php b/tests/front/controller/admin/ExportControllerTest.php index 50d9e378..12d26f4a 100644 --- a/tests/front/controller/admin/ExportControllerTest.php +++ b/tests/front/controller/admin/ExportControllerTest.php @@ -84,7 +84,7 @@ function ( 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; } diff --git a/tests/front/controller/admin/ToolsControllerTest.php b/tests/front/controller/admin/ToolsControllerTest.php index fc756f0f..39144d2f 100644 --- a/tests/front/controller/admin/ToolsControllerTest.php +++ b/tests/front/controller/admin/ToolsControllerTest.php @@ -8,7 +8,7 @@ use Slim\Http\Request; use Slim\Http\Response; -class ToolsControllerTestControllerTest extends TestCase +class ToolsControllerTest extends TestCase { use FrontAdminControllerMockHelper; @@ -41,7 +41,7 @@ public function testDefaultInvokeWithHttps(): void 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 @@ public function testDefaultInvokeWithoutHttps(): void 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']); } } diff --git a/tests/front/controller/visitor/DailyControllerTest.php b/tests/front/controller/visitor/DailyControllerTest.php index b802c62c..cb5b96f3 100644 --- a/tests/front/controller/visitor/DailyControllerTest.php +++ b/tests/front/controller/visitor/DailyControllerTest.php @@ -392,8 +392,8 @@ public function testValidRssControllerInvokeDefault(): void 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 @@ public function testValidRssControllerInvokeDefault(): void 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 @@ public function testValidRssControllerInvokeDefault(): void 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 @@ public function testValidRssControllerInvokeNoBookmark(): void 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']); } diff --git a/tests/front/controller/visitor/FrontControllerMockHelper.php b/tests/front/controller/visitor/FrontControllerMockHelper.php index 927e7f0a..6c53289b 100644 --- a/tests/front/controller/visitor/FrontControllerMockHelper.php +++ b/tests/front/controller/visitor/FrontControllerMockHelper.php @@ -79,8 +79,9 @@ protected function createContainer(): void $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'; diff --git a/tests/front/controller/visitor/InstallControllerTest.php b/tests/front/controller/visitor/InstallControllerTest.php index 3b855365..994d3f33 100644 --- a/tests/front/controller/visitor/InstallControllerTest.php +++ b/tests/front/controller/visitor/InstallControllerTest.php @@ -257,6 +257,39 @@ public function testSaveInstallDefaultValues(): void 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']); } } diff --git a/tests/front/controller/visitor/OpenSearchControllerTest.php b/tests/front/controller/visitor/OpenSearchControllerTest.php index 5f9f5b12..9609a377 100644 --- a/tests/front/controller/visitor/OpenSearchControllerTest.php +++ b/tests/front/controller/visitor/OpenSearchControllerTest.php @@ -39,6 +39,6 @@ public function testOpenSearchController(): void $result->getHeader('Content-Type')[0] ); static::assertSame('opensearch', (string) $result->getBody()); - static::assertSame('http://shaarli', $assignedVariables['serverurl']); + static::assertSame('http://shaarli/subfolder/', $assignedVariables['serverurl']); } } diff --git a/tests/http/HttpUtils/IndexUrlTest.php b/tests/http/HttpUtils/IndexUrlTest.php index 73d33cd4..cce45c51 100644 --- a/tests/http/HttpUtils/IndexUrlTest.php +++ b/tests/http/HttpUtils/IndexUrlTest.php @@ -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 @@ public function testPageUrlWithRoute() ) ); } + + /** + * 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 index 00000000..15ca3d72 --- /dev/null +++ b/tests/http/HttpUtils/IndexUrlTestWithConstant.php @@ -0,0 +1,51 @@ +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', + ) + ) + ); + } +}