aboutsummaryrefslogtreecommitdiff
path: root/nixops/modules/websites/tools
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2019-02-07 01:36:13 +0100
committerIsmaël Bouya <ismael.bouya@normalesup.org>2019-02-07 01:36:13 +0100
commit95b20e17b97de1f05392d0c394a5efdc590311a9 (patch)
treecdfbefec201c0e05668353fc87c80669dbe66215 /nixops/modules/websites/tools
parent609c29615905e2bc5628a164cea2c28ec70d5d35 (diff)
downloadNix-95b20e17b97de1f05392d0c394a5efdc590311a9.tar.gz
Nix-95b20e17b97de1f05392d0c394a5efdc590311a9.tar.zst
Nix-95b20e17b97de1f05392d0c394a5efdc590311a9.zip
Add shaarli website
Diffstat (limited to 'nixops/modules/websites/tools')
-rw-r--r--nixops/modules/websites/tools/tools/default.nix9
-rw-r--r--nixops/modules/websites/tools/tools/shaarli.nix86
-rw-r--r--nixops/modules/websites/tools/tools/shaarli_ldap.patch420
3 files changed, 514 insertions, 1 deletions
diff --git a/nixops/modules/websites/tools/tools/default.nix b/nixops/modules/websites/tools/tools/default.nix
index 333ffb0..41f47a3 100644
--- a/nixops/modules/websites/tools/tools/default.nix
+++ b/nixops/modules/websites/tools/tools/default.nix
@@ -18,6 +18,9 @@ let
18 inherit (mylibs) fetchedGithub; 18 inherit (mylibs) fetchedGithub;
19 env = myconfig.env.tools.rompr; 19 env = myconfig.env.tools.rompr;
20 }; 20 };
21 shaarli = pkgs.callPackage ./shaarli.nix {
22 env = myconfig.env.tools.shaarli;
23 };
21 24
22 cfg = config.services.myWebsites.tools.tools; 25 cfg = config.services.myWebsites.tools.tools;
23in { 26in {
@@ -35,7 +38,8 @@ in {
35 ++ roundcubemail.apache.modules 38 ++ roundcubemail.apache.modules
36 ++ wallabag.apache.modules 39 ++ wallabag.apache.modules
37 ++ yourls.apache.modules 40 ++ yourls.apache.modules
38 ++ rompr.apache.modules; 41 ++ rompr.apache.modules
42 ++ shaarli.apache.modules;
39 43
40 services.ympd = ympd.config // { enable = true; }; 44 services.ympd = ympd.config // { enable = true; };
41 45
@@ -51,6 +55,7 @@ in {
51 wallabag.apache.vhostConf 55 wallabag.apache.vhostConf
52 yourls.apache.vhostConf 56 yourls.apache.vhostConf
53 rompr.apache.vhostConf 57 rompr.apache.vhostConf
58 shaarli.apache.vhostConf
54 ]; 59 ];
55 }; 60 };
56 61
@@ -61,6 +66,7 @@ in {
61 wallabag = wallabag.phpFpm.pool; 66 wallabag = wallabag.phpFpm.pool;
62 yourls = yourls.phpFpm.pool; 67 yourls = yourls.phpFpm.pool;
63 rompr = rompr.phpFpm.pool; 68 rompr = rompr.phpFpm.pool;
69 shaarli = shaarli.phpFpm.pool;
64 }; 70 };
65 71
66 system.activationScripts = { 72 system.activationScripts = {
@@ -69,6 +75,7 @@ in {
69 wallabag = wallabag.activationScript; 75 wallabag = wallabag.activationScript;
70 yourls = yourls.activationScript; 76 yourls = yourls.activationScript;
71 rompr = rompr.activationScript; 77 rompr = rompr.activationScript;
78 shaarli = shaarli.activationScript;
72 }; 79 };
73 80
74 systemd.services.tt-rss = { 81 systemd.services.tt-rss = {
diff --git a/nixops/modules/websites/tools/tools/shaarli.nix b/nixops/modules/websites/tools/tools/shaarli.nix
new file mode 100644
index 0000000..9f3779f
--- /dev/null
+++ b/nixops/modules/websites/tools/tools/shaarli.nix
@@ -0,0 +1,86 @@
1{ lib, env, stdenv, fetchurl }:
2
3let
4 varDir = "/var/lib/shaarli";
5 shaarli = stdenv.mkDerivation rec {
6 name = "shaarli-${version}";
7 version = "0.10.2";
8
9 src = fetchurl {
10 url = "https://github.com/shaarli/Shaarli/releases/download/v${version}/shaarli-v${version}-full.tar.gz";
11 sha256 = "0h8sspj7siy3vgpi2i3gdrjcr5935fr4dfwq2zwd70sjx2sh9s78";
12 };
13
14 outputs = [ "out" "doc" ];
15
16 patches = [ ./shaarli_ldap.patch ];
17
18 installPhase = ''
19 rm -r {cache,pagecache,tmp,data}/
20 ln -sf ../../../..${varDir}/{cache,pagecache,tmp,data} .
21 mkdir -p $doc/share/doc
22 mv doc/ $doc/share/doc/shaarli
23 mkdir $out/
24 cp -R ./* $out
25 cp .htaccess $out/
26 '';
27
28 meta = with stdenv.lib; {
29 description = "The personal, minimalist, super-fast, database free, bookmarking service";
30 license = licenses.gpl3Plus;
31 homepage = https://github.com/shaarli/Shaarli;
32 maintainers = with maintainers; [ schneefux ];
33 platforms = platforms.all;
34 };
35 };
36in rec {
37 activationScript = ''
38 install -m 0755 -o ${apache.user} -g ${apache.group} -d ${varDir} \
39 ${varDir}/cache ${varDir}/pagecache ${varDir}/tmp ${varDir}/data \
40 ${varDir}/phpSessions
41 '';
42 webRoot = shaarli;
43 apache = {
44 user = "wwwrun";
45 group = "wwwrun";
46 modules = [ "proxy_fcgi" "rewrite" "env" ];
47 vhostConf = ''
48 Alias /Shaarli "${webRoot}"
49
50 <Directory "${webRoot}">
51 SetEnv SHAARLI_LDAP_PASSWORD "${env.ldap.password}"
52 SetEnv SHAARLI_LDAP_DN "${env.ldap.dn}"
53 SetEnv SHAARLI_LDAP_HOST "ldaps://${env.ldap.host}"
54 SetEnv SHAARLI_LDAP_BASE "${env.ldap.base}"
55 SetEnv SHAARLI_LDAP_FILTER "${env.ldap.search}"
56
57 DirectoryIndex index.php index.htm index.html
58 Options Indexes FollowSymLinks MultiViews Includes
59 AllowOverride All
60 Require all granted
61 <FilesMatch "\.php$">
62 SetHandler "proxy:unix:${phpFpm.socket}|fcgi://localhost"
63 </FilesMatch>
64 </Directory>
65 '';
66 };
67 phpFpm = rec {
68 basedir = builtins.concatStringsSep ":" [ webRoot varDir ];
69 socket = "/var/run/phpfpm/shaarli.sock";
70 pool = ''
71 listen = ${socket}
72 user = ${apache.user}
73 group = ${apache.group}
74 listen.owner = ${apache.user}
75 listen.group = ${apache.group}
76 pm = ondemand
77 pm.max_children = 60
78 pm.process_idle_timeout = 60
79
80 ; Needed to avoid clashes in browser cookies (same domain)
81 php_value[session.name] = ShaarliPHPSESSID
82 php_admin_value[open_basedir] = "${basedir}:/tmp"
83 php_admin_value[session.save_path] = "${varDir}/phpSessions"
84 '';
85 };
86}
diff --git a/nixops/modules/websites/tools/tools/shaarli_ldap.patch b/nixops/modules/websites/tools/tools/shaarli_ldap.patch
new file mode 100644
index 0000000..9c7315a
--- /dev/null
+++ b/nixops/modules/websites/tools/tools/shaarli_ldap.patch
@@ -0,0 +1,420 @@
1commit bc82ebfd779b8641dadd6787f51639ea9105c3e8
2Author: Ismaël Bouya <ismael.bouya@normalesup.org>
3Date: Sun Feb 3 20:58:18 2019 +0100
4
5 Add ldap connection
6
7diff --git a/.htaccess b/.htaccess
8index 4c00427..5acd708 100644
9--- a/.htaccess
10+++ b/.htaccess
11@@ -6,10 +6,23 @@ RewriteEngine On
12 # Prevent accessing subdirectories not managed by SCM
13 RewriteRule ^(.git|doxygen|vendor) - [F]
14
15+RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$
16+RewriteRule ^(.*) - [E=BASE:%1]
17+
18+RewriteCond %{ENV:REDIRECT_BASE} (.+)
19+RewriteRule .* - [E=BASE:%1]
20+
21 # Forward the "Authorization" HTTP header
22 RewriteCond %{HTTP:Authorization} ^(.*)
23 RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
24
25+RewriteCond %{REQUEST_FILENAME} !-f
26+RewriteCond %{REQUEST_FILENAME} !-d
27+RewriteRule ^((?!api/)[^/]*)/?(.*)$ $2?%{QUERY_STRING} [E=USERSPACE:$1]
28+
29+RewriteCond %{ENV:REDIRECT_USERSPACE} (.+)
30+RewriteRule .* - [E=USERSPACE:%1]
31+
32 # REST API
33 RewriteCond %{REQUEST_FILENAME} !-f
34 RewriteCond %{REQUEST_FILENAME} !-d
35diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php
36index 911873a..f21a1ef 100644
37--- a/application/ApplicationUtils.php
38+++ b/application/ApplicationUtils.php
39@@ -191,6 +191,9 @@ public static function checkResourcePermissions($conf)
40 $conf->get('resource.page_cache'),
41 $conf->get('resource.raintpl_tmp'),
42 ) as $path) {
43+ if (! is_dir($path)) {
44+ mkdir($path, 0755, true);
45+ }
46 if (! is_readable(realpath($path))) {
47 $errors[] = '"'.$path.'" '. t('directory is not readable');
48 }
49diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php
50index 32aaea4..99efc15 100644
51--- a/application/config/ConfigManager.php
52+++ b/application/config/ConfigManager.php
53@@ -21,6 +21,11 @@ class ConfigManager
54
55 public static $DEFAULT_PLUGINS = array('qrcode');
56
57+ /**
58+ * @var string User space.
59+ */
60+ protected $userSpace;
61+
62 /**
63 * @var string Config folder.
64 */
65@@ -41,12 +46,36 @@ class ConfigManager
66 *
67 * @param string $configFile Configuration file path without extension.
68 */
69- public function __construct($configFile = 'data/config')
70+ public function __construct($configFile = null, $userSpace = null)
71 {
72- $this->configFile = $configFile;
73+ $this->userSpace = $this->findLDAPUser($userSpace);
74+ if ($configFile !== null) {
75+ $this->configFile = $configFile;
76+ } else {
77+ $this->configFile = ($this->userSpace === null) ? 'data/config' : 'data/' . $this->userSpace . '/config';
78+ }
79 $this->initialize();
80 }
81
82+ public function findLDAPUser($login, $password = null) {
83+ $connect = ldap_connect(getenv('SHAARLI_LDAP_HOST'));
84+ ldap_set_option($connect, LDAP_OPT_PROTOCOL_VERSION, 3);
85+ if (!$connect || !ldap_bind($connect, getenv('SHAARLI_LDAP_DN'), getenv('SHAARLI_LDAP_PASSWORD'))) {
86+ return false;
87+ }
88+
89+ $search_query = str_replace('%login%', ldap_escape($login), getenv('SHAARLI_LDAP_FILTER'));
90+
91+ $search = ldap_search($connect, getenv('SHAARLI_LDAP_BASE'), $search_query);
92+ $info = ldap_get_entries($connect, $search);
93+
94+ if (ldap_count_entries($connect, $search) == 1 && (is_null($password) || ldap_bind($connect, $info[0]["dn"], $password))) {
95+ return $login;
96+ } else {
97+ return null;
98+ }
99+ }
100+
101 /**
102 * Reset the ConfigManager instance.
103 */
104@@ -269,6 +298,16 @@ public function getConfigFileExt()
105 return $this->configFile . $this->configIO->getExtension();
106 }
107
108+ /**
109+ * Get the current userspace.
110+ *
111+ * @return mixed User space.
112+ */
113+ public function getUserSpace()
114+ {
115+ return $this->userSpace;
116+ }
117+
118 /**
119 * Recursive function which find asked setting in the loaded config.
120 *
121@@ -342,19 +381,31 @@ protected static function removeConfig($settings, &$conf)
122 */
123 protected function setDefaultValues()
124 {
125- $this->setEmpty('resource.data_dir', 'data');
126- $this->setEmpty('resource.config', 'data/config.php');
127- $this->setEmpty('resource.datastore', 'data/datastore.php');
128- $this->setEmpty('resource.ban_file', 'data/ipbans.php');
129- $this->setEmpty('resource.updates', 'data/updates.txt');
130- $this->setEmpty('resource.log', 'data/log.txt');
131- $this->setEmpty('resource.update_check', 'data/lastupdatecheck.txt');
132- $this->setEmpty('resource.history', 'data/history.php');
133+ if ($this->userSpace === null) {
134+ $data = 'data';
135+ $tmp = 'tmp';
136+ $cache = 'cache';
137+ $pagecache = 'pagecache';
138+ } else {
139+ $data = 'data/' . ($this->userSpace);
140+ $tmp = 'tmp/' . ($this->userSpace);
141+ $cache = 'cache/' . ($this->userSpace);
142+ $pagecache = 'pagecache/' . ($this->userSpace);
143+ }
144+
145+ $this->setEmpty('resource.data_dir', $data);
146+ $this->setEmpty('resource.config', $data . '/config.php');
147+ $this->setEmpty('resource.datastore', $data . '/datastore.php');
148+ $this->setEmpty('resource.ban_file', $data . '/ipbans.php');
149+ $this->setEmpty('resource.updates', $data . '/updates.txt');
150+ $this->setEmpty('resource.log', $data . '/log.txt');
151+ $this->setEmpty('resource.update_check', $data . '/lastupdatecheck.txt');
152+ $this->setEmpty('resource.history', $data . '/history.php');
153 $this->setEmpty('resource.raintpl_tpl', 'tpl/');
154 $this->setEmpty('resource.theme', 'default');
155- $this->setEmpty('resource.raintpl_tmp', 'tmp/');
156- $this->setEmpty('resource.thumbnails_cache', 'cache');
157- $this->setEmpty('resource.page_cache', 'pagecache');
158+ $this->setEmpty('resource.raintpl_tmp', $tmp);
159+ $this->setEmpty('resource.thumbnails_cache', $cache);
160+ $this->setEmpty('resource.page_cache', $pagecache);
161
162 $this->setEmpty('security.ban_after', 4);
163 $this->setEmpty('security.ban_duration', 1800);
164diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php
165index d6784d6..bdfaca7 100644
166--- a/application/security/LoginManager.php
167+++ b/application/security/LoginManager.php
168@@ -32,6 +32,9 @@ class LoginManager
169 /** @var string User sign-in token depending on remote IP and credentials */
170 protected $staySignedInToken = '';
171
172+ protected $lastErrorReason = '';
173+ protected $lastErrorIsBanishable = false;
174+
175 /**
176 * Constructor
177 *
178@@ -83,7 +86,7 @@ public function getStaySignedInToken()
179 */
180 public function checkLoginState($cookie, $clientIpId)
181 {
182- if (! $this->configManager->exists('credentials.login')) {
183+ if (! $this->configManager->exists('credentials.login') || (isset($_SESSION['username']) && $_SESSION['username'] && $this->configManager->get('credentials.login') !== $_SESSION['username'])) {
184 // Shaarli is not configured yet
185 $this->isLoggedIn = false;
186 return;
187@@ -133,20 +136,40 @@ public function isLoggedIn()
188 */
189 public function checkCredentials($remoteIp, $clientIpId, $login, $password)
190 {
191- $hash = sha1($password . $login . $this->configManager->get('credentials.salt'));
192+ $this->lastErrorIsBanishable = false;
193+
194+ if ($this->configManager->getUserSpace() !== null && $this->configManager->getUserSpace() !== $login) {
195+ logm($this->configManager->get('resource.log'),
196+ $remoteIp,
197+ 'Trying to login to wrong user space');
198+ $this->lastErrorReason = 'You’re trying to access the wrong account.';
199+ return false;
200+ }
201
202- if ($login != $this->configManager->get('credentials.login')
203- || $hash != $this->configManager->get('credentials.hash')
204- ) {
205+ logm($this->configManager->get('resource.log'),
206+ $remoteIp,
207+ 'Trying LDAP connection');
208+ $result = $this->configManager->findLDAPUser($login, $password);
209+ if ($result === false) {
210 logm(
211 $this->configManager->get('resource.log'),
212 $remoteIp,
213- 'Login failed for user ' . $login
214+ 'Impossible to connect to LDAP'
215 );
216+ $this->lastErrorReason = 'Server error.';
217+ return false;
218+ } else if (is_null($result)) {
219+ logm(
220+ $this->configManager->get('resource.log'),
221+ $remoteIp,
222+ 'Login failed for user ' . $login
223+ );
224+ $this->lastErrorIsBanishable = true;
225+ $this->lastErrorReason = 'Wrong login/password.';
226 return false;
227 }
228
229- $this->sessionManager->storeLoginInfo($clientIpId);
230+ $this->sessionManager->storeLoginInfo($clientIpId, $login);
231 logm(
232 $this->configManager->get('resource.log'),
233 $remoteIp,
234@@ -187,6 +210,10 @@ protected function writeBanFile()
235 */
236 public function handleFailedLogin($server)
237 {
238+ if (!$this->lastErrorIsBanishable) {
239+ return $this->lastErrorReason ?: 'Error during login.';
240+ };
241+
242 $ip = $server['REMOTE_ADDR'];
243 $trusted = $this->configManager->get('security.trusted_proxies', []);
244
245@@ -215,6 +242,7 @@ public function handleFailedLogin($server)
246 );
247 }
248 $this->writeBanFile();
249+ return $this->lastErrorReason ?: 'Error during login.';
250 }
251
252 /**
253diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php
254index b8b8ab8..5eb4aac 100644
255--- a/application/security/SessionManager.php
256+++ b/application/security/SessionManager.php
257@@ -111,10 +111,10 @@ public static function checkId($sessionId)
258 *
259 * @param string $clientIpId Client IP address identifier
260 */
261- public function storeLoginInfo($clientIpId)
262+ public function storeLoginInfo($clientIpId, $login = null)
263 {
264 $this->session['ip'] = $clientIpId;
265- $this->session['username'] = $this->conf->get('credentials.login');
266+ $this->session['username'] = $login ?: $this->conf->get('credentials.login');
267 $this->extendTimeValidityBy(self::$SHORT_TIMEOUT);
268 }
269
270diff --git a/index.php b/index.php
271index 4b86a3e..85376e8 100644
272--- a/index.php
273+++ b/index.php
274@@ -121,7 +121,27 @@
275 $_COOKIE['shaarli'] = session_id();
276 }
277
278-$conf = new ConfigManager();
279+$folderBase = getenv("BASE");
280+
281+if (getenv("USERSPACE")) {
282+ if (isset($_GET["do"]) && $_GET["do"] == "login") {
283+ header("Location: $folderBase/?do=login");
284+ exit;
285+ }
286+ $userspace = preg_replace("/[^-_A-Za-z0-9]/", '', getenv("USERSPACE"));
287+} else if (isset($_SESSION["username"]) && $_SESSION["username"]) {
288+ header("Location: " . $folderBase . "/" . $_SESSION["username"] . "?");
289+ exit;
290+} else if (!isset($_GET["do"]) || $_GET["do"] != "login") {
291+ header("Location: $folderBase/?do=login");
292+ exit;
293+}
294+
295+if (isset($userspace)) {
296+ $conf = new ConfigManager(null, $userspace);
297+} else {
298+ $conf = new ConfigManager();
299+}
300 $sessionManager = new SessionManager($_SESSION, $conf);
301 $loginManager = new LoginManager($GLOBALS, $conf, $sessionManager);
302 $loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
303@@ -175,7 +195,7 @@
304 }
305
306 // Display the installation form if no existing config is found
307- install($conf, $sessionManager, $loginManager);
308+ install($conf, $sessionManager, $loginManager, $userspace);
309 }
310
311 $loginManager->checkLoginState($_COOKIE, $clientIpId);
312@@ -205,6 +225,7 @@ function isLoggedIn()
313 && $loginManager->checkCredentials($_SERVER['REMOTE_ADDR'], $clientIpId, $_POST['login'], $_POST['password'])
314 ) {
315 $loginManager->handleSuccessfulLogin($_SERVER);
316+ $userspace = $_POST['login'];
317
318 $cookiedir = '';
319 if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
320@@ -241,25 +262,25 @@ function isLoggedIn()
321 $uri .= '&'.$param.'='.urlencode($_GET[$param]);
322 }
323 }
324- header('Location: '. $uri);
325+ header('Location: '. $userspace . $uri);
326 exit;
327 }
328
329 if (isset($_GET['edit_link'])) {
330- header('Location: ?edit_link='. escape($_GET['edit_link']));
331+ header('Location: ' . $userspace . '?edit_link='. escape($_GET['edit_link']));
332 exit;
333 }
334
335 if (isset($_POST['returnurl'])) {
336 // Prevent loops over login screen.
337 if (strpos($_POST['returnurl'], 'do=login') === false) {
338- header('Location: '. generateLocation($_POST['returnurl'], $_SERVER['HTTP_HOST']));
339+ header('Location: ' . generateLocation($_POST['returnurl'], $_SERVER['HTTP_HOST']));
340 exit;
341 }
342 }
343- header('Location: ?'); exit;
344+ header('Location: '. $userspace . '?'); exit;
345 } else {
346- $loginManager->handleFailedLogin($_SERVER);
347+ $errorReason = $loginManager->handleFailedLogin($_SERVER);
348 $redir = '&username='. urlencode($_POST['login']);
349 if (isset($_GET['post'])) {
350 $redir .= '&post=' . urlencode($_GET['post']);
351@@ -270,7 +291,7 @@ function isLoggedIn()
352 }
353 }
354 // Redirect to login screen.
355- echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'?do=login'.$redir.'\';</script>';
356+ echo '<script>alert("'. t($errorReason) .'");document.location=\'?do=login'.$redir.'\';</script>';
357 exit;
358 }
359 }
360@@ -1719,7 +1740,7 @@ function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
361 * @param SessionManager $sessionManager SessionManager instance
362 * @param LoginManager $loginManager LoginManager instance
363 */
364-function install($conf, $sessionManager, $loginManager) {
365+function install($conf, $sessionManager, $loginManager, $userspace) {
366 // On free.fr host, make sure the /sessions directory exists, otherwise login will not work.
367 if (endsWith($_SERVER['HTTP_HOST'],'.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions',0705);
368
369@@ -1755,7 +1776,7 @@ function install($conf, $sessionManager, $loginManager) {
370 }
371
372
373- if (!empty($_POST['setlogin']) && !empty($_POST['setpassword']))
374+ if (true)
375 {
376 $tz = 'UTC';
377 if (!empty($_POST['continent']) && !empty($_POST['city'])
378@@ -1764,15 +1785,15 @@ function install($conf, $sessionManager, $loginManager) {
379 $tz = $_POST['continent'].'/'.$_POST['city'];
380 }
381 $conf->set('general.timezone', $tz);
382- $login = $_POST['setlogin'];
383- $conf->set('credentials.login', $login);
384+ $conf->set('credentials.login', $userspace);
385 $salt = sha1(uniqid('', true) .'_'. mt_rand());
386 $conf->set('credentials.salt', $salt);
387- $conf->set('credentials.hash', sha1($_POST['setpassword'] . $login . $salt));
388+ $hash = sha1(uniqid('', true) .'_'. mt_rand());
389+ $conf->set('credentials.hash', $hash);
390 if (!empty($_POST['title'])) {
391 $conf->set('general.title', escape($_POST['title']));
392 } else {
393- $conf->set('general.title', 'Shared links on '.escape(index_url($_SERVER)));
394+ $conf->set('general.title', ucwords(str_replace("_", " ", $userspace)));
395 }
396 $conf->set('translation.language', escape($_POST['language']));
397 $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
398@@ -1841,7 +1862,12 @@ function install($conf, $sessionManager, $loginManager) {
399 $app = new \Slim\App($container);
400
401 // REST API routes
402-$app->group('/api/v1', function() {
403+if (isset($userspace)) {
404+ $mountpoint = '/' . $userspace . '/api/v1';
405+} else {
406+ $mountpoint = '/api/v1';
407+}
408+$app->group($mountpoint, function() {
409 $this->get('/info', '\Shaarli\Api\Controllers\Info:getInfo')->setName('getInfo');
410 $this->get('/links', '\Shaarli\Api\Controllers\Links:getLinks')->setName('getLinks');
411 $this->get('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:getLink')->setName('getLink');
412@@ -1860,7 +1886,7 @@ function install($conf, $sessionManager, $loginManager) {
413 $response = $app->run(true);
414 // Hack to make Slim and Shaarli router work together:
415 // If a Slim route isn't found and NOT API call, we call renderPage().
416-if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) {
417+if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], $mountpoint) === false) {
418 // We use UTF-8 for proper international characters handling.
419 header('Content-Type: text/html; charset=utf-8');
420 renderPage($conf, $pluginManager, $linkDb, $history, $sessionManager, $loginManager);