aboutsummaryrefslogtreecommitdiffhomepage
path: root/index.php
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2018-07-28 11:07:55 +0200
committerArthurHoaro <arthur@hoa.ro>2018-07-28 11:07:55 +0200
commit83faedadff76c5bdca036f39f13943f63b27e164 (patch)
tree6f44cede16ec6a60f10b9699e211e0818f06d2c8 /index.php
parent1d9eb22a3df85b67fe6652c0876cd7382c2fb525 (diff)
parent658988f3aeba7a5a938783249ccf2765251e5597 (diff)
downloadShaarli-83faedadff76c5bdca036f39f13943f63b27e164.tar.gz
Shaarli-83faedadff76c5bdca036f39f13943f63b27e164.tar.zst
Shaarli-83faedadff76c5bdca036f39f13943f63b27e164.zip
Merge tag 'v0.9.7' into stable
Release v0.9.7
Diffstat (limited to 'index.php')
-rw-r--r--index.php823
1 files changed, 466 insertions, 357 deletions
diff --git a/index.php b/index.php
index 7c5b0f8e..e05055c3 100644
--- a/index.php
+++ b/index.php
@@ -1,8 +1,12 @@
1<?php 1<?php
2/** 2/**
3<<<<<<< HEAD
3 * Shaarli v0.8.7 - Shaare your links... 4 * Shaarli v0.8.7 - Shaare your links...
4 * 5 *
5 * The personal, minimalist, super-fast, database free, bookmarking service. 6 * The personal, minimalist, super-fast, database free, bookmarking service.
7=======
8 * Shaarli - The personal, minimalist, super-fast, database free, bookmarking service.
9>>>>>>> v0.9.7
6 * 10 *
7 * Friendly fork by the Shaarli community: 11 * Friendly fork by the Shaarli community:
8 * - https://github.com/shaarli/Shaarli 12 * - https://github.com/shaarli/Shaarli
@@ -13,7 +17,7 @@
13 * 17 *
14 * Licence: http://www.opensource.org/licenses/zlib-license.php 18 * Licence: http://www.opensource.org/licenses/zlib-license.php
15 * 19 *
16 * Requires: PHP 5.3.x 20 * Requires: PHP 5.5.x
17 */ 21 */
18 22
19// Set 'UTC' as the default timezone if it is not defined in php.ini 23// Set 'UTC' as the default timezone if it is not defined in php.ini
@@ -25,7 +29,6 @@ if (date_default_timezone_get() == '') {
25/* 29/*
26 * PHP configuration 30 * PHP configuration
27 */ 31 */
28define('shaarli_version', '0.8.7');
29 32
30// http://server.com/x/shaarli --> /shaarli/ 33// http://server.com/x/shaarli --> /shaarli/
31define('WEB_PATH', substr($_SERVER['REQUEST_URI'], 0, 1+strrpos($_SERVER['REQUEST_URI'], '/', 0))); 34define('WEB_PATH', substr($_SERVER['REQUEST_URI'], 0, 1+strrpos($_SERVER['REQUEST_URI'], '/', 0)));
@@ -51,8 +54,8 @@ if (! file_exists(__DIR__ . '/vendor/autoload.php')) {
51 ."If you installed Shaarli through Git or using the development branch,\n" 54 ."If you installed Shaarli through Git or using the development branch,\n"
52 ."please refer to the installation documentation to install PHP" 55 ."please refer to the installation documentation to install PHP"
53 ." dependencies using Composer:\n" 56 ." dependencies using Composer:\n"
54 ."- https://github.com/shaarli/Shaarli/wiki/Server-requirements\n" 57 ."- https://shaarli.readthedocs.io/en/master/Server-requirements/\n"
55 ."- https://github.com/shaarli/Shaarli/wiki/Download-and-Installation"; 58 ."- https://shaarli.readthedocs.io/en/master/Download-and-Installation/";
56 exit; 59 exit;
57} 60}
58require_once 'inc/rain.tpl.class.php'; 61require_once 'inc/rain.tpl.class.php';
@@ -62,12 +65,11 @@ require_once __DIR__ . '/vendor/autoload.php';
62require_once 'application/ApplicationUtils.php'; 65require_once 'application/ApplicationUtils.php';
63require_once 'application/Cache.php'; 66require_once 'application/Cache.php';
64require_once 'application/CachedPage.php'; 67require_once 'application/CachedPage.php';
65require_once 'application/config/ConfigManager.php';
66require_once 'application/config/ConfigPlugin.php'; 68require_once 'application/config/ConfigPlugin.php';
67require_once 'application/FeedBuilder.php'; 69require_once 'application/FeedBuilder.php';
68require_once 'application/FileUtils.php'; 70require_once 'application/FileUtils.php';
71require_once 'application/History.php';
69require_once 'application/HttpUtils.php'; 72require_once 'application/HttpUtils.php';
70require_once 'application/Languages.php';
71require_once 'application/LinkDB.php'; 73require_once 'application/LinkDB.php';
72require_once 'application/LinkFilter.php'; 74require_once 'application/LinkFilter.php';
73require_once 'application/LinkUtils.php'; 75require_once 'application/LinkUtils.php';
@@ -79,16 +81,22 @@ require_once 'application/Utils.php';
79require_once 'application/PluginManager.php'; 81require_once 'application/PluginManager.php';
80require_once 'application/Router.php'; 82require_once 'application/Router.php';
81require_once 'application/Updater.php'; 83require_once 'application/Updater.php';
84use \Shaarli\Languages;
85use \Shaarli\ThemeUtils;
86use \Shaarli\Config\ConfigManager;
87use \Shaarli\SessionManager;
82 88
83// Ensure the PHP version is supported 89// Ensure the PHP version is supported
84try { 90try {
85 ApplicationUtils::checkPHPVersion('5.3', PHP_VERSION); 91 ApplicationUtils::checkPHPVersion('5.5', PHP_VERSION);
86} catch(Exception $exc) { 92} catch(Exception $exc) {
87 header('Content-Type: text/plain; charset=utf-8'); 93 header('Content-Type: text/plain; charset=utf-8');
88 echo $exc->getMessage(); 94 echo $exc->getMessage();
89 exit; 95 exit;
90} 96}
91 97
98define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE));
99
92// Force cookie path (but do not change lifetime) 100// Force cookie path (but do not change lifetime)
93$cookie = session_get_cookie_params(); 101$cookie = session_get_cookie_params();
94$cookiedir = ''; 102$cookiedir = '';
@@ -114,15 +122,29 @@ if (session_id() == '') {
114} 122}
115 123
116// Regenerate session ID if invalid or not defined in cookie. 124// Regenerate session ID if invalid or not defined in cookie.
117if (isset($_COOKIE['shaarli']) && !is_session_id_valid($_COOKIE['shaarli'])) { 125if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) {
118 session_regenerate_id(true); 126 session_regenerate_id(true);
119 $_COOKIE['shaarli'] = session_id(); 127 $_COOKIE['shaarli'] = session_id();
120} 128}
121 129
122$conf = new ConfigManager(); 130$conf = new ConfigManager();
131$sessionManager = new SessionManager($_SESSION, $conf);
132
133// LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead.
134if (! defined('LC_MESSAGES')) {
135 define('LC_MESSAGES', LC_COLLATE);
136}
137
138// Sniff browser language and set date format accordingly.
139if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
140 autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']);
141}
142
143new Languages(setlocale(LC_MESSAGES, 0), $conf);
144
123$conf->setEmpty('general.timezone', date_default_timezone_get()); 145$conf->setEmpty('general.timezone', date_default_timezone_get());
124$conf->setEmpty('general.title', 'Shared links on '. escape(index_url($_SERVER))); 146$conf->setEmpty('general.title', t('Shared links on '). escape(index_url($_SERVER)));
125RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl'); // template directory 147RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory
126RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory 148RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory
127 149
128$pluginManager = new PluginManager($conf); 150$pluginManager = new PluginManager($conf);
@@ -132,15 +154,6 @@ date_default_timezone_set($conf->get('general.timezone', 'UTC'));
132 154
133ob_start(); // Output buffering for the page cache. 155ob_start(); // Output buffering for the page cache.
134 156
135// In case stupid admin has left magic_quotes enabled in php.ini:
136if (get_magic_quotes_gpc())
137{
138 function stripslashes_deep($value) { $value = is_array($value) ? array_map('stripslashes_deep', $value) : stripslashes($value); return $value; }
139 $_POST = array_map('stripslashes_deep', $_POST);
140 $_GET = array_map('stripslashes_deep', $_GET);
141 $_COOKIE = array_map('stripslashes_deep', $_COOKIE);
142}
143
144// Prevent caching on client side or proxy: (yes, it's ugly) 157// Prevent caching on client side or proxy: (yes, it's ugly)
145header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); 158header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
146header("Cache-Control: no-store, no-cache, must-revalidate"); 159header("Cache-Control: no-store, no-cache, must-revalidate");
@@ -152,7 +165,7 @@ if (! is_file($conf->getConfigFileExt())) {
152 $errors = ApplicationUtils::checkResourcePermissions($conf); 165 $errors = ApplicationUtils::checkResourcePermissions($conf);
153 166
154 if ($errors != array()) { 167 if ($errors != array()) {
155 $message = '<p>Insufficient permissions:</p><ul>'; 168 $message = '<p>'. t('Insufficient permissions:') .'</p><ul>';
156 169
157 foreach ($errors as $error) { 170 foreach ($errors as $error) {
158 $message .= '<li>'.$error.'</li>'; 171 $message .= '<li>'.$error.'</li>';
@@ -165,18 +178,12 @@ if (! is_file($conf->getConfigFileExt())) {
165 } 178 }
166 179
167 // Display the installation form if no existing config is found 180 // Display the installation form if no existing config is found
168 install($conf); 181 install($conf, $sessionManager);
169} 182}
170 183
171// a token depending of deployment salt, user password, and the current ip 184// a token depending of deployment salt, user password, and the current ip
172define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt'))); 185define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt')));
173 186
174// Sniff browser language and set date format accordingly.
175if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
176 autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']);
177}
178header('Content-Type: text/html; charset=utf-8'); // We use UTF-8 for proper international characters handling.
179
180/** 187/**
181 * Checking session state (i.e. is the user still logged in) 188 * Checking session state (i.e. is the user still logged in)
182 * 189 *
@@ -186,65 +193,44 @@ header('Content-Type: text/html; charset=utf-8'); // We use UTF-8 for proper int
186 */ 193 */
187function setup_login_state($conf) 194function setup_login_state($conf)
188{ 195{
189 if ($conf->get('security.open_shaarli')) { 196 if ($conf->get('security.open_shaarli')) {
190 return true; 197 return true;
191 } 198 }
192 $userIsLoggedIn = false; // By default, we do not consider the user as logged in; 199 $userIsLoggedIn = false; // By default, we do not consider the user as logged in;
193 $loginFailure = false; // If set to true, every attempt to authenticate the user will fail. This indicates that an important condition isn't met. 200 $loginFailure = false; // If set to true, every attempt to authenticate the user will fail. This indicates that an important condition isn't met.
194 if (! $conf->exists('credentials.login')) { 201 if (! $conf->exists('credentials.login')) {
195 $userIsLoggedIn = false; // Shaarli is not configured yet. 202 $userIsLoggedIn = false; // Shaarli is not configured yet.
196 $loginFailure = true; 203 $loginFailure = true;
197 } 204 }
198 if (isset($_COOKIE['shaarli_staySignedIn']) && 205 if (isset($_COOKIE['shaarli_staySignedIn']) &&
199 $_COOKIE['shaarli_staySignedIn']===STAY_SIGNED_IN_TOKEN && 206 $_COOKIE['shaarli_staySignedIn']===STAY_SIGNED_IN_TOKEN &&
200 !$loginFailure) 207 !$loginFailure)
201 { 208 {
202 fillSessionInfo($conf); 209 fillSessionInfo($conf);
203 $userIsLoggedIn = true; 210 $userIsLoggedIn = true;
204 } 211 }
205 // If session does not exist on server side, or IP address has changed, or session has expired, logout. 212 // If session does not exist on server side, or IP address has changed, or session has expired, logout.
206 if (empty($_SESSION['uid']) 213 if (empty($_SESSION['uid'])
207 || ($conf->get('security.session_protection_disabled') == false && $_SESSION['ip'] != allIPs()) 214 || ($conf->get('security.session_protection_disabled') === false && $_SESSION['ip'] != allIPs())
208 || time() >= $_SESSION['expires_on']) 215 || time() >= $_SESSION['expires_on'])
209 {
210 logout();
211 $userIsLoggedIn = false;
212 $loginFailure = true;
213 }
214 if (!empty($_SESSION['longlastingsession'])) {
215 $_SESSION['expires_on']=time()+$_SESSION['longlastingsession']; // In case of "Stay signed in" checked.
216 }
217 else {
218 $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Standard session expiration date.
219 }
220 if (!$loginFailure) {
221 $userIsLoggedIn = true;
222 }
223
224 return $userIsLoggedIn;
225}
226$userIsLoggedIn = setup_login_state($conf);
227
228/**
229 * PubSubHubbub protocol support (if enabled) [UNTESTED]
230 * (Source: http://aldarone.fr/les-flux-rss-shaarli-et-pubsubhubbub/ )
231 *
232 * @param ConfigManager $conf Configuration Manager instance.
233 */
234function pubsubhub($conf)
235{
236 $pshUrl = $conf->get('config.PUBSUBHUB_URL');
237 if (!empty($pshUrl))
238 { 216 {
239 include_once './publisher.php'; 217 logout();
240 $p = new Publisher($pshUrl); 218 $userIsLoggedIn = false;
241 $topic_url = array ( 219 $loginFailure = true;
242 index_url($_SERVER).'?do=atom', 220 }
243 index_url($_SERVER).'?do=rss' 221 if (!empty($_SESSION['longlastingsession'])) {
244 ); 222 $_SESSION['expires_on']=time()+$_SESSION['longlastingsession']; // In case of "Stay signed in" checked.
245 $p->publish_update($topic_url); 223 }
224 else {
225 $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Standard session expiration date.
246 } 226 }
227 if (!$loginFailure) {
228 $userIsLoggedIn = true;
229 }
230
231 return $userIsLoggedIn;
247} 232}
233$userIsLoggedIn = setup_login_state($conf);
248 234
249// ------------------------------------------------------------------------------------------ 235// ------------------------------------------------------------------------------------------
250// Session management 236// Session management
@@ -266,10 +252,10 @@ function allIPs()
266 */ 252 */
267function fillSessionInfo($conf) 253function fillSessionInfo($conf)
268{ 254{
269 $_SESSION['uid'] = sha1(uniqid('',true).'_'.mt_rand()); // Generate unique random number (different than phpsessionid) 255 $_SESSION['uid'] = sha1(uniqid('',true).'_'.mt_rand()); // Generate unique random number (different than phpsessionid)
270 $_SESSION['ip']=allIPs(); // We store IP address(es) of the client to make sure session is not hijacked. 256 $_SESSION['ip']=allIPs(); // We store IP address(es) of the client to make sure session is not hijacked.
271 $_SESSION['username']= $conf->get('credentials.login'); 257 $_SESSION['username']= $conf->get('credentials.login');
272 $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Set session expiration. 258 $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Set session expiration.
273} 259}
274 260
275/** 261/**
@@ -286,7 +272,7 @@ function check_auth($login, $password, $conf)
286 $hash = sha1($password . $login . $conf->get('credentials.salt')); 272 $hash = sha1($password . $login . $conf->get('credentials.salt'));
287 if ($login == $conf->get('credentials.login') && $hash == $conf->get('credentials.hash')) 273 if ($login == $conf->get('credentials.login') && $hash == $conf->get('credentials.hash'))
288 { // Login/password is correct. 274 { // Login/password is correct.
289 fillSessionInfo($conf); 275 fillSessionInfo($conf);
290 logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login successful'); 276 logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login successful');
291 return true; 277 return true;
292 } 278 }
@@ -308,6 +294,7 @@ function logout() {
308 unset($_SESSION['ip']); 294 unset($_SESSION['ip']);
309 unset($_SESSION['username']); 295 unset($_SESSION['username']);
310 unset($_SESSION['privateonly']); 296 unset($_SESSION['privateonly']);
297 unset($_SESSION['untaggedonly']);
311 } 298 }
312 setcookie('shaarli_staySignedIn', FALSE, 0, WEB_PATH); 299 setcookie('shaarli_staySignedIn', FALSE, 0, WEB_PATH);
313} 300}
@@ -405,18 +392,19 @@ function ban_canLogin($conf)
405// Process login form: Check if login/password is correct. 392// Process login form: Check if login/password is correct.
406if (isset($_POST['login'])) 393if (isset($_POST['login']))
407{ 394{
408 if (!ban_canLogin($conf)) die('I said: NO. You are banned for the moment. Go away.'); 395 if (!ban_canLogin($conf)) die(t('I said: NO. You are banned for the moment. Go away.'));
409 if (isset($_POST['password']) 396 if (isset($_POST['password'])
410 && tokenOk($_POST['token']) 397 && $sessionManager->checkToken($_POST['token'])
411 && (check_auth($_POST['login'], $_POST['password'], $conf)) 398 && (check_auth($_POST['login'], $_POST['password'], $conf))
412 ) { // Login/password is OK. 399 ) { // Login/password is OK.
413 ban_loginOk($conf); 400 ban_loginOk($conf);
414 // If user wants to keep the session cookie even after the browser closes: 401 // If user wants to keep the session cookie even after the browser closes:
415 if (!empty($_POST['longlastingsession'])) 402 if (!empty($_POST['longlastingsession']))
416 { 403 {
417 setcookie('shaarli_staySignedIn', STAY_SIGNED_IN_TOKEN, time()+31536000, WEB_PATH); 404 $_SESSION['longlastingsession'] = 31536000; // (31536000 seconds = 1 year)
418 $_SESSION['longlastingsession']=31536000; // (31536000 seconds = 1 year) 405 $expiration = time() + $_SESSION['longlastingsession']; // calculate relative cookie expiration (1 year from now)
419 $_SESSION['expires_on']=time()+$_SESSION['longlastingsession']; // Set session expiration on server-side. 406 setcookie('shaarli_staySignedIn', STAY_SIGNED_IN_TOKEN, $expiration, WEB_PATH);
407 $_SESSION['expires_on'] = $expiration; // Set session expiration on server-side.
420 408
421 $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/'; 409 $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/';
422 session_set_cookie_params($_SESSION['longlastingsession'],$cookiedir,$_SERVER['SERVER_NAME']); // Set session cookie expiration on client side 410 session_set_cookie_params($_SESSION['longlastingsession'],$cookiedir,$_SERVER['SERVER_NAME']); // Set session cookie expiration on client side
@@ -433,7 +421,7 @@ if (isset($_POST['login']))
433 // Optional redirect after login: 421 // Optional redirect after login:
434 if (isset($_GET['post'])) { 422 if (isset($_GET['post'])) {
435 $uri = '?post='. urlencode($_GET['post']); 423 $uri = '?post='. urlencode($_GET['post']);
436 foreach (array('description', 'source', 'title') as $param) { 424 foreach (array('description', 'source', 'title', 'tags') as $param) {
437 if (!empty($_GET[$param])) { 425 if (!empty($_GET[$param])) {
438 $uri .= '&'.$param.'='.urlencode($_GET[$param]); 426 $uri .= '&'.$param.'='.urlencode($_GET[$param]);
439 } 427 }
@@ -462,77 +450,24 @@ if (isset($_POST['login']))
462 $redir = '&username='. urlencode($_POST['login']); 450 $redir = '&username='. urlencode($_POST['login']);
463 if (isset($_GET['post'])) { 451 if (isset($_GET['post'])) {
464 $redir .= '&post=' . urlencode($_GET['post']); 452 $redir .= '&post=' . urlencode($_GET['post']);
465 foreach (array('description', 'source', 'title') as $param) { 453 foreach (array('description', 'source', 'title', 'tags') as $param) {
466 if (!empty($_GET[$param])) { 454 if (!empty($_GET[$param])) {
467 $redir .= '&' . $param . '=' . urlencode($_GET[$param]); 455 $redir .= '&' . $param . '=' . urlencode($_GET[$param]);
468 } 456 }
469 } 457 }
470 } 458 }
471 echo '<script>alert("Wrong login/password.");document.location=\'?do=login'.$redir.'\';</script>'; // Redirect to login screen. 459 // Redirect to login screen.
460 echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'?do=login'.$redir.'\';</script>';
472 exit; 461 exit;
473 } 462 }
474} 463}
475 464
476// ------------------------------------------------------------------------------------------ 465// ------------------------------------------------------------------------------------------
477// Misc utility functions:
478
479// Convert post_max_size/upload_max_filesize (e.g. '16M') parameters to bytes.
480function return_bytes($val)
481{
482 $val = trim($val); $last=strtolower($val[strlen($val)-1]);
483 switch($last)
484 {
485 case 'g': $val *= 1024;
486 case 'm': $val *= 1024;
487 case 'k': $val *= 1024;
488 }
489 return $val;
490}
491
492// Try to determine max file size for uploads (POST).
493// Returns an integer (in bytes)
494function getMaxFileSize()
495{
496 $size1 = return_bytes(ini_get('post_max_size'));
497 $size2 = return_bytes(ini_get('upload_max_filesize'));
498 // Return the smaller of two:
499 $maxsize = min($size1,$size2);
500 // FIXME: Then convert back to readable notations ? (e.g. 2M instead of 2000000)
501 return $maxsize;
502}
503
504// ------------------------------------------------------------------------------------------
505// Token management for XSRF protection 466// Token management for XSRF protection
506// Token should be used in any form which acts on data (create,update,delete,import...). 467// Token should be used in any form which acts on data (create,update,delete,import...).
507if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are attached to the session. 468if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are attached to the session.
508 469
509/** 470/**
510 * Returns a token.
511 *
512 * @param ConfigManager $conf Configuration Manager instance.
513 *
514 * @return string token.
515 */
516function getToken($conf)
517{
518 $rnd = sha1(uniqid('', true) .'_'. mt_rand() . $conf->get('credentials.salt')); // We generate a random string.
519 $_SESSION['tokens'][$rnd]=1; // Store it on the server side.
520 return $rnd;
521}
522
523// Tells if a token is OK. Using this function will destroy the token.
524// true=token is OK.
525function tokenOk($token)
526{
527 if (isset($_SESSION['tokens'][$token]))
528 {
529 unset($_SESSION['tokens'][$token]); // Token is used: destroy it.
530 return true; // Token is OK.
531 }
532 return false; // Wrong token, or already used.
533}
534
535/**
536 * Daily RSS feed: 1 RSS entry per day giving all the links on that day. 471 * Daily RSS feed: 1 RSS entry per day giving all the links on that day.
537 * Gives the last 7 days (which have links). 472 * Gives the last 7 days (which have links).
538 * This RSS feed cannot be filtered. 473 * This RSS feed cannot be filtered.
@@ -602,7 +537,11 @@ function showDailyRSS($conf) {
602 537
603 // We pre-format some fields for proper output. 538 // We pre-format some fields for proper output.
604 foreach ($links as &$link) { 539 foreach ($links as &$link) {
605 $link['formatedDescription'] = format_description($link['description'], $conf->get('redirector.url')); 540 $link['formatedDescription'] = format_description(
541 $link['description'],
542 $conf->get('redirector.url'),
543 $conf->get('redirector.encode_url')
544 );
606 $link['thumbnail'] = thumbnail($conf, $link['url']); 545 $link['thumbnail'] = thumbnail($conf, $link['url']);
607 $link['timestamp'] = $link['created']->getTimestamp(); 546 $link['timestamp'] = $link['created']->getTimestamp();
608 if (startsWith($link['url'], '?')) { 547 if (startsWith($link['url'], '?')) {
@@ -618,7 +557,7 @@ function showDailyRSS($conf) {
618 $tpl->assign('links', $links); 557 $tpl->assign('links', $links);
619 $tpl->assign('rssdate', escape($dayDate->format(DateTime::RSS))); 558 $tpl->assign('rssdate', escape($dayDate->format(DateTime::RSS)));
620 $tpl->assign('hide_timestamps', $conf->get('privacy.hide_timestamps', false)); 559 $tpl->assign('hide_timestamps', $conf->get('privacy.hide_timestamps', false));
621 $html = $tpl->draw('dailyrss', $return_string=true); 560 $html = $tpl->draw('dailyrss', true);
622 561
623 echo $html . PHP_EOL; 562 echo $html . PHP_EOL;
624 } 563 }
@@ -639,20 +578,29 @@ function showDailyRSS($conf) {
639 */ 578 */
640function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) 579function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
641{ 580{
642 $day=date('Ymd',strtotime('-1 day')); // Yesterday, in format YYYYMMDD. 581 $day = date('Ymd', strtotime('-1 day')); // Yesterday, in format YYYYMMDD.
643 if (isset($_GET['day'])) $day=$_GET['day']; 582 if (isset($_GET['day'])) {
583 $day = $_GET['day'];
584 }
644 585
645 $days = $LINKSDB->days(); 586 $days = $LINKSDB->days();
646 $i = array_search($day,$days); 587 $i = array_search($day, $days);
647 if ($i===false) { $i=count($days)-1; $day=$days[$i]; } 588 if ($i === false && count($days)) {
648 $previousday=''; 589 // no links for day, but at least one day with links
649 $nextday=''; 590 $i = count($days) - 1;
650 if ($i!==false) 591 $day = $days[$i];
651 {
652 if ($i>=1) $previousday=$days[$i-1];
653 if ($i<count($days)-1) $nextday=$days[$i+1];
654 } 592 }
593 $previousday = '';
594 $nextday = '';
655 595
596 if ($i !== false) {
597 if ($i >= 1) {
598 $previousday=$days[$i - 1];
599 }
600 if ($i < count($days) - 1) {
601 $nextday = $days[$i + 1];
602 }
603 }
656 try { 604 try {
657 $linksToDisplay = $LINKSDB->filterDay($day); 605 $linksToDisplay = $LINKSDB->filterDay($day);
658 } catch (Exception $exc) { 606 } catch (Exception $exc) {
@@ -661,13 +609,15 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
661 } 609 }
662 610
663 // We pre-format some fields for proper output. 611 // We pre-format some fields for proper output.
664 foreach($linksToDisplay as $key=>$link) 612 foreach($linksToDisplay as $key => $link) {
665 {
666
667 $taglist = explode(' ',$link['tags']); 613 $taglist = explode(' ',$link['tags']);
668 uasort($taglist, 'strcasecmp'); 614 uasort($taglist, 'strcasecmp');
669 $linksToDisplay[$key]['taglist']=$taglist; 615 $linksToDisplay[$key]['taglist']=$taglist;
670 $linksToDisplay[$key]['formatedDescription'] = format_description($link['description'], $conf->get('redirector.url')); 616 $linksToDisplay[$key]['formatedDescription'] = format_description(
617 $link['description'],
618 $conf->get('redirector.url'),
619 $conf->get('redirector.encode_url')
620 );
671 $linksToDisplay[$key]['thumbnail'] = thumbnail($conf, $link['url']); 621 $linksToDisplay[$key]['thumbnail'] = thumbnail($conf, $link['url']);
672 $linksToDisplay[$key]['timestamp'] = $link['created']->getTimestamp(); 622 $linksToDisplay[$key]['timestamp'] = $link['created']->getTimestamp();
673 } 623 }
@@ -677,28 +627,31 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
677 so I manually spread entries with a simple method: I roughly evaluate the 627 so I manually spread entries with a simple method: I roughly evaluate the
678 height of a div according to title and description length. 628 height of a div according to title and description length.
679 */ 629 */
680 $columns=array(array(),array(),array()); // Entries to display, for each column. 630 $columns = array(array(), array(), array()); // Entries to display, for each column.
681 $fill=array(0,0,0); // Rough estimate of columns fill. 631 $fill = array(0, 0, 0); // Rough estimate of columns fill.
682 foreach($linksToDisplay as $key=>$link) 632 foreach($linksToDisplay as $key => $link) {
683 {
684 // Roughly estimate length of entry (by counting characters) 633 // Roughly estimate length of entry (by counting characters)
685 // Title: 30 chars = 1 line. 1 line is 30 pixels height. 634 // Title: 30 chars = 1 line. 1 line is 30 pixels height.
686 // Description: 836 characters gives roughly 342 pixel height. 635 // Description: 836 characters gives roughly 342 pixel height.
687 // This is not perfect, but it's usually OK. 636 // This is not perfect, but it's usually OK.
688 $length=strlen($link['title'])+(342*strlen($link['description']))/836; 637 $length = strlen($link['title']) + (342 * strlen($link['description'])) / 836;
689 if ($link['thumbnail']) $length +=100; // 1 thumbnails roughly takes 100 pixels height. 638 if ($link['thumbnail']) {
639 $length += 100; // 1 thumbnails roughly takes 100 pixels height.
640 }
690 // Then put in column which is the less filled: 641 // Then put in column which is the less filled:
691 $smallest=min($fill); // find smallest value in array. 642 $smallest = min($fill); // find smallest value in array.
692 $index=array_search($smallest,$fill); // find index of this smallest value. 643 $index = array_search($smallest, $fill); // find index of this smallest value.
693 array_push($columns[$index],$link); // Put entry in this column. 644 array_push($columns[$index], $link); // Put entry in this column.
694 $fill[$index]+=$length; 645 $fill[$index] += $length;
695 } 646 }
696 647
697 $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000'); 648 $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000');
698 $data = array( 649 $data = array(
650 'pagetitle' => $conf->get('general.title') .' - '. format_date($dayDate, false),
699 'linksToDisplay' => $linksToDisplay, 651 'linksToDisplay' => $linksToDisplay,
700 'cols' => $columns, 652 'cols' => $columns,
701 'day' => $dayDate->getTimestamp(), 653 'day' => $dayDate->getTimestamp(),
654 'dayDate' => $dayDate,
702 'previousday' => $previousday, 655 'previousday' => $previousday,
703 'nextday' => $nextday, 656 'nextday' => $nextday,
704 ); 657 );
@@ -729,19 +682,14 @@ function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager) {
729/** 682/**
730 * Render HTML page (according to URL parameters and user rights) 683 * Render HTML page (according to URL parameters and user rights)
731 * 684 *
732 * @param ConfigManager $conf Configuration Manager instance. 685 * @param ConfigManager $conf Configuration Manager instance.
733 * @param PluginManager $pluginManager Plugin Manager instance, 686 * @param PluginManager $pluginManager Plugin Manager instance,
687 * @param LinkDB $LINKSDB
688 * @param History $history instance
689 * @param SessionManager $sessionManager SessionManager instance
734 */ 690 */
735function renderPage($conf, $pluginManager) 691function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
736{ 692{
737 $LINKSDB = new LinkDB(
738 $conf->get('resource.datastore'),
739 isLoggedIn(),
740 $conf->get('privacy.hide_public_links'),
741 $conf->get('redirector.url'),
742 $conf->get('redirector.encode_url')
743 );
744
745 $updater = new Updater( 693 $updater = new Updater(
746 read_updates_file($conf->get('resource.updates')), 694 read_updates_file($conf->get('resource.updates')),
747 $LINKSDB, 695 $LINKSDB,
@@ -761,7 +709,7 @@ function renderPage($conf, $pluginManager)
761 die($e->getMessage()); 709 die($e->getMessage());
762 } 710 }
763 711
764 $PAGE = new PageBuilder($conf); 712 $PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken());
765 $PAGE->assign('linkcount', count($LINKSDB)); 713 $PAGE->assign('linkcount', count($LINKSDB));
766 $PAGE->assign('privateLinkcount', count_private($LINKSDB)); 714 $PAGE->assign('privateLinkcount', count_private($LINKSDB));
767 $PAGE->assign('plugin_errors', $pluginManager->getErrors()); 715 $PAGE->assign('plugin_errors', $pluginManager->getErrors());
@@ -770,6 +718,23 @@ function renderPage($conf, $pluginManager)
770 $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : ''; 718 $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : '';
771 $targetPage = Router::findPage($query, $_GET, isLoggedIn()); 719 $targetPage = Router::findPage($query, $_GET, isLoggedIn());
772 720
721 if (
722 // if the user isn't logged in
723 !isLoggedIn() &&
724 // and Shaarli doesn't have public content...
725 $conf->get('privacy.hide_public_links') &&
726 // and is configured to enforce the login
727 $conf->get('privacy.force_login') &&
728 // and the current page isn't already the login page
729 $targetPage !== Router::$PAGE_LOGIN &&
730 // and the user is not requesting a feed (which would lead to a different content-type as expected)
731 $targetPage !== Router::$PAGE_FEED_ATOM &&
732 $targetPage !== Router::$PAGE_FEED_RSS
733 ) {
734 // force current page to be the login page
735 $targetPage = Router::$PAGE_LOGIN;
736 }
737
773 // Call plugin hooks for header, footer and includes, specifying which page will be rendered. 738 // Call plugin hooks for header, footer and includes, specifying which page will be rendered.
774 // Then assign generated data to RainTPL. 739 // Then assign generated data to RainTPL.
775 $common_hooks = array( 740 $common_hooks = array(
@@ -797,6 +762,8 @@ function renderPage($conf, $pluginManager)
797 $PAGE->assign('username', escape($_GET['username'])); 762 $PAGE->assign('username', escape($_GET['username']));
798 } 763 }
799 $PAGE->assign('returnurl',(isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']):'')); 764 $PAGE->assign('returnurl',(isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']):''));
765 // add default state of the 'remember me' checkbox
766 $PAGE->assign('remember_user_default', $conf->get('privacy.remember_user_default'));
800 $PAGE->renderPage('loginform'); 767 $PAGE->renderPage('loginform');
801 exit; 768 exit;
802 } 769 }
@@ -844,7 +811,9 @@ function renderPage($conf, $pluginManager)
844 // -------- Tag cloud 811 // -------- Tag cloud
845 if ($targetPage == Router::$PAGE_TAGCLOUD) 812 if ($targetPage == Router::$PAGE_TAGCLOUD)
846 { 813 {
847 $tags= $LINKSDB->allTags(); 814 $visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all';
815 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
816 $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility);
848 817
849 // We sort tags alphabetically, then choose a font size according to count. 818 // We sort tags alphabetically, then choose a font size according to count.
850 // First, find max value. 819 // First, find max value.
@@ -853,20 +822,13 @@ function renderPage($conf, $pluginManager)
853 $maxcount = max($maxcount, $value); 822 $maxcount = max($maxcount, $value);
854 } 823 }
855 824
856 // Sort tags alphabetically: case insensitive, support locale if available. 825 alphabetical_sort($tags, false, true);
857 uksort($tags, function($a, $b) {
858 // Collator is part of PHP intl.
859 if (class_exists('Collator')) {
860 $c = new Collator(setlocale(LC_COLLATE, 0));
861 if (!intl_is_failure(intl_get_error_code())) {
862 return $c->compare($a, $b);
863 }
864 }
865 return strcasecmp($a, $b);
866 });
867 826
868 $tagList = array(); 827 $tagList = array();
869 foreach($tags as $key => $value) { 828 foreach($tags as $key => $value) {
829 if (in_array($key, $filteringTags)) {
830 continue;
831 }
870 // Tag font size scaling: 832 // Tag font size scaling:
871 // default 15 and 30 logarithm bases affect scaling, 833 // default 15 and 30 logarithm bases affect scaling,
872 // 22 and 6 are arbitrary font sizes for max and min sizes. 834 // 22 and 6 are arbitrary font sizes for max and min sizes.
@@ -878,6 +840,7 @@ function renderPage($conf, $pluginManager)
878 } 840 }
879 841
880 $data = array( 842 $data = array(
843 'search_tags' => implode(' ', escape($filteringTags)),
881 'tags' => $tagList, 844 'tags' => $tagList,
882 ); 845 );
883 $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn())); 846 $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn()));
@@ -886,7 +849,37 @@ function renderPage($conf, $pluginManager)
886 $PAGE->assign($key, $value); 849 $PAGE->assign($key, $value);
887 } 850 }
888 851
889 $PAGE->renderPage('tagcloud'); 852 $PAGE->renderPage('tag.cloud');
853 exit;
854 }
855
856 // -------- Tag list
857 if ($targetPage == Router::$PAGE_TAGLIST)
858 {
859 $visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all';
860 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
861 $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility);
862 foreach ($filteringTags as $tag) {
863 if (array_key_exists($tag, $tags)) {
864 unset($tags[$tag]);
865 }
866 }
867
868 if (! empty($_GET['sort']) && $_GET['sort'] === 'alpha') {
869 alphabetical_sort($tags, false, true);
870 }
871
872 $data = [
873 'search_tags' => implode(' ', escape($filteringTags)),
874 'tags' => $tags,
875 ];
876 $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]);
877
878 foreach ($data as $key => $value) {
879 $PAGE->assign($key, $value);
880 }
881
882 $PAGE->renderPage('tag.list');
890 exit; 883 exit;
891 } 884 }
892 885
@@ -918,10 +911,6 @@ function renderPage($conf, $pluginManager)
918 $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0))); 911 $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0)));
919 $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !isLoggedIn()); 912 $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !isLoggedIn());
920 $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks')); 913 $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks'));
921 $pshUrl = $conf->get('config.PUBSUBHUB_URL');
922 if (!empty($pshUrl)) {
923 $feedGenerator->setPubsubhubUrl($pshUrl);
924 }
925 $data = $feedGenerator->buildData(); 914 $data = $feedGenerator->buildData();
926 915
927 // Process plugin hook. 916 // Process plugin hook.
@@ -938,7 +927,7 @@ function renderPage($conf, $pluginManager)
938 exit; 927 exit;
939 } 928 }
940 929
941 // Display openseach plugin (XML) 930 // Display opensearch plugin (XML)
942 if ($targetPage == Router::$PAGE_OPENSEARCH) { 931 if ($targetPage == Router::$PAGE_OPENSEARCH) {
943 header('Content-Type: application/xml; charset=utf-8'); 932 header('Content-Type: application/xml; charset=utf-8');
944 $PAGE->assign('serverurl', index_url($_SERVER)); 933 $PAGE->assign('serverurl', index_url($_SERVER));
@@ -1023,7 +1012,12 @@ function renderPage($conf, $pluginManager)
1023 $_SESSION['LINKS_PER_PAGE']=abs(intval($_GET['linksperpage'])); 1012 $_SESSION['LINKS_PER_PAGE']=abs(intval($_GET['linksperpage']));
1024 } 1013 }
1025 1014
1026 header('Location: '. generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('linksperpage'))); 1015 if (! empty($_SERVER['HTTP_REFERER'])) {
1016 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('linksperpage'));
1017 } else {
1018 $location = '?';
1019 }
1020 header('Location: '. $location);
1027 exit; 1021 exit;
1028 } 1022 }
1029 1023
@@ -1035,7 +1029,25 @@ function renderPage($conf, $pluginManager)
1035 unset($_SESSION['privateonly']); // See all links 1029 unset($_SESSION['privateonly']); // See all links
1036 } 1030 }
1037 1031
1038 header('Location: '. generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('privateonly'))); 1032 if (! empty($_SERVER['HTTP_REFERER'])) {
1033 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('privateonly'));
1034 } else {
1035 $location = '?';
1036 }
1037 header('Location: '. $location);
1038 exit;
1039 }
1040
1041 // -------- User wants to see only untagged links (toggle)
1042 if (isset($_GET['untaggedonly'])) {
1043 $_SESSION['untaggedonly'] = empty($_SESSION['untaggedonly']);
1044
1045 if (! empty($_SERVER['HTTP_REFERER'])) {
1046 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('untaggedonly'));
1047 } else {
1048 $location = '?';
1049 }
1050 header('Location: '. $location);
1039 exit; 1051 exit;
1040 } 1052 }
1041 1053
@@ -1046,7 +1058,13 @@ function renderPage($conf, $pluginManager)
1046 // Show login screen, then redirect to ?post=... 1058 // Show login screen, then redirect to ?post=...
1047 if (isset($_GET['post'])) 1059 if (isset($_GET['post']))
1048 { 1060 {
1049 header('Location: ?do=login&post='.urlencode($_GET['post']).(!empty($_GET['title'])?'&title='.urlencode($_GET['title']):'').(!empty($_GET['description'])?'&description='.urlencode($_GET['description']):'').(!empty($_GET['source'])?'&source='.urlencode($_GET['source']):'')); // Redirect to login page, then back to post link. 1061 header( // Redirect to login page, then back to post link.
1062 'Location: ?do=login&post='.urlencode($_GET['post']).
1063 (!empty($_GET['title'])?'&title='.urlencode($_GET['title']):'').
1064 (!empty($_GET['description'])?'&description='.urlencode($_GET['description']):'').
1065 (!empty($_GET['tags'])?'&tags='.urlencode($_GET['tags']):'').
1066 (!empty($_GET['source'])?'&source='.urlencode($_GET['source']):'')
1067 );
1050 exit; 1068 exit;
1051 } 1069 }
1052 1070
@@ -1064,10 +1082,10 @@ function renderPage($conf, $pluginManager)
1064 // -------- Display the Tools menu if requested (import/export/bookmarklet...) 1082 // -------- Display the Tools menu if requested (import/export/bookmarklet...)
1065 if ($targetPage == Router::$PAGE_TOOLS) 1083 if ($targetPage == Router::$PAGE_TOOLS)
1066 { 1084 {
1067 $data = array( 1085 $data = [
1068 'pageabsaddr' => index_url($_SERVER), 1086 'pageabsaddr' => index_url($_SERVER),
1069 'sslenabled' => !empty($_SERVER['HTTPS']) 1087 'sslenabled' => is_https($_SERVER),
1070 ); 1088 ];
1071 $pluginManager->executeHooks('render_tools', $data); 1089 $pluginManager->executeHooks('render_tools', $data);
1072 1090
1073 foreach ($data as $key => $value) { 1091 foreach ($data as $key => $value) {
@@ -1082,16 +1100,19 @@ function renderPage($conf, $pluginManager)
1082 if ($targetPage == Router::$PAGE_CHANGEPASSWORD) 1100 if ($targetPage == Router::$PAGE_CHANGEPASSWORD)
1083 { 1101 {
1084 if ($conf->get('security.open_shaarli')) { 1102 if ($conf->get('security.open_shaarli')) {
1085 die('You are not supposed to change a password on an Open Shaarli.'); 1103 die(t('You are not supposed to change a password on an Open Shaarli.'));
1086 } 1104 }
1087 1105
1088 if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword'])) 1106 if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword']))
1089 { 1107 {
1090 if (!tokenOk($_POST['token'])) die('Wrong token.'); // Go away! 1108 if (!$sessionManager->checkToken($_POST['token'])) die(t('Wrong token.')); // Go away!
1091 1109
1092 // Make sure old password is correct. 1110 // Make sure old password is correct.
1093 $oldhash = sha1($_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt')); 1111 $oldhash = sha1($_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt'));
1094 if ($oldhash!= $conf->get('credentials.hash')) { echo '<script>alert("The old password is not correct.");document.location=\'?do=changepasswd\';</script>'; exit; } 1112 if ($oldhash!= $conf->get('credentials.hash')) {
1113 echo '<script>alert("'. t('The old password is not correct.') .'");document.location=\'?do=changepasswd\';</script>';
1114 exit;
1115 }
1095 // Save new password 1116 // Save new password
1096 // Salt renders rainbow-tables attacks useless. 1117 // Salt renders rainbow-tables attacks useless.
1097 $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand())); 1118 $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
@@ -1109,7 +1130,7 @@ function renderPage($conf, $pluginManager)
1109 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=tools\';</script>'; 1130 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=tools\';</script>';
1110 exit; 1131 exit;
1111 } 1132 }
1112 echo '<script>alert("Your password has been changed.");document.location=\'?do=tools\';</script>'; 1133 echo '<script>alert("'. t('Your password has been changed') .'");document.location=\'?do=tools\';</script>';
1113 exit; 1134 exit;
1114 } 1135 }
1115 else // show the change password form. 1136 else // show the change password form.
@@ -1124,8 +1145,8 @@ function renderPage($conf, $pluginManager)
1124 { 1145 {
1125 if (!empty($_POST['title']) ) 1146 if (!empty($_POST['title']) )
1126 { 1147 {
1127 if (!tokenOk($_POST['token'])) { 1148 if (!$sessionManager->checkToken($_POST['token'])) {
1128 die('Wrong token.'); // Go away! 1149 die(t('Wrong token.')); // Go away!
1129 } 1150 }
1130 $tz = 'UTC'; 1151 $tz = 'UTC';
1131 if (!empty($_POST['continent']) && !empty($_POST['city']) 1152 if (!empty($_POST['continent']) && !empty($_POST['city'])
@@ -1136,14 +1157,21 @@ function renderPage($conf, $pluginManager)
1136 $conf->set('general.timezone', $tz); 1157 $conf->set('general.timezone', $tz);
1137 $conf->set('general.title', escape($_POST['title'])); 1158 $conf->set('general.title', escape($_POST['title']));
1138 $conf->set('general.header_link', escape($_POST['titleLink'])); 1159 $conf->set('general.header_link', escape($_POST['titleLink']));
1160 $conf->set('resource.theme', escape($_POST['theme']));
1139 $conf->set('redirector.url', escape($_POST['redirector'])); 1161 $conf->set('redirector.url', escape($_POST['redirector']));
1140 $conf->set('security.session_protection_disabled', !empty($_POST['disablesessionprotection'])); 1162 $conf->set('security.session_protection_disabled', !empty($_POST['disablesessionprotection']));
1141 $conf->set('privacy.default_private_links', !empty($_POST['privateLinkByDefault'])); 1163 $conf->set('privacy.default_private_links', !empty($_POST['privateLinkByDefault']));
1142 $conf->set('feed.rss_permalinks', !empty($_POST['enableRssPermalinks'])); 1164 $conf->set('feed.rss_permalinks', !empty($_POST['enableRssPermalinks']));
1143 $conf->set('updates.check_updates', !empty($_POST['updateCheck'])); 1165 $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
1144 $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks'])); 1166 $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks']));
1167 $conf->set('api.enabled', !empty($_POST['enableApi']));
1168 $conf->set('api.secret', escape($_POST['apiSecret']));
1169 $conf->set('translation.language', escape($_POST['language']));
1170
1145 try { 1171 try {
1146 $conf->write(isLoggedIn()); 1172 $conf->write(isLoggedIn());
1173 $history->updateSettings();
1174 invalidateCaches($conf->get('resource.page_cache'));
1147 } 1175 }
1148 catch(Exception $e) { 1176 catch(Exception $e) {
1149 error_log( 1177 error_log(
@@ -1155,21 +1183,30 @@ function renderPage($conf, $pluginManager)
1155 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=configure\';</script>'; 1183 echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=configure\';</script>';
1156 exit; 1184 exit;
1157 } 1185 }
1158 echo '<script>alert("Configuration was saved.");document.location=\'?do=configure\';</script>'; 1186 echo '<script>alert("'. t('Configuration was saved.') .'");document.location=\'?do=configure\';</script>';
1159 exit; 1187 exit;
1160 } 1188 }
1161 else // Show the configuration form. 1189 else // Show the configuration form.
1162 { 1190 {
1163 $PAGE->assign('title', $conf->get('general.title')); 1191 $PAGE->assign('title', $conf->get('general.title'));
1192 $PAGE->assign('theme', $conf->get('resource.theme'));
1193 $PAGE->assign('theme_available', ThemeUtils::getThemes($conf->get('resource.raintpl_tpl')));
1164 $PAGE->assign('redirector', $conf->get('redirector.url')); 1194 $PAGE->assign('redirector', $conf->get('redirector.url'));
1165 list($timezone_form, $timezone_js) = generateTimeZoneForm($conf->get('general.timezone')); 1195 list($continents, $cities) = generateTimeZoneData(
1166 $PAGE->assign('timezone_form', $timezone_form); 1196 timezone_identifiers_list(),
1167 $PAGE->assign('timezone_js',$timezone_js); 1197 $conf->get('general.timezone')
1198 );
1199 $PAGE->assign('continents', $continents);
1200 $PAGE->assign('cities', $cities);
1168 $PAGE->assign('private_links_default', $conf->get('privacy.default_private_links', false)); 1201 $PAGE->assign('private_links_default', $conf->get('privacy.default_private_links', false));
1169 $PAGE->assign('session_protection_disabled', $conf->get('security.session_protection_disabled', false)); 1202 $PAGE->assign('session_protection_disabled', $conf->get('security.session_protection_disabled', false));
1170 $PAGE->assign('enable_rss_permalinks', $conf->get('feed.rss_permalinks', false)); 1203 $PAGE->assign('enable_rss_permalinks', $conf->get('feed.rss_permalinks', false));
1171 $PAGE->assign('enable_update_check', $conf->get('updates.check_updates', true)); 1204 $PAGE->assign('enable_update_check', $conf->get('updates.check_updates', true));
1172 $PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false)); 1205 $PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false));
1206 $PAGE->assign('api_enabled', $conf->get('api.enabled', true));
1207 $PAGE->assign('api_secret', $conf->get('api.secret'));
1208 $PAGE->assign('languages', Languages::getAvailableLanguages());
1209 $PAGE->assign('language', $conf->get('translation.language'));
1173 $PAGE->renderPage('configure'); 1210 $PAGE->renderPage('configure');
1174 exit; 1211 exit;
1175 } 1212 }
@@ -1179,48 +1216,28 @@ function renderPage($conf, $pluginManager)
1179 if ($targetPage == Router::$PAGE_CHANGETAG) 1216 if ($targetPage == Router::$PAGE_CHANGETAG)
1180 { 1217 {
1181 if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) { 1218 if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) {
1182 $PAGE->assign('tags', $LINKSDB->allTags()); 1219 $PAGE->assign('fromtag', ! empty($_GET['fromtag']) ? escape($_GET['fromtag']) : '');
1183 $PAGE->renderPage('changetag'); 1220 $PAGE->renderPage('changetag');
1184 exit; 1221 exit;
1185 } 1222 }
1186 1223
1187 if (!tokenOk($_POST['token'])) { 1224 if (!$sessionManager->checkToken($_POST['token'])) {
1188 die('Wrong token.'); 1225 die(t('Wrong token.'));
1189 } 1226 }
1190 1227
1191 // Delete a tag: 1228 $alteredLinks = $LINKSDB->renameTag(escape($_POST['fromtag']), escape($_POST['totag']));
1192 if (isset($_POST['deletetag']) && !empty($_POST['fromtag'])) { 1229 $LINKSDB->save($conf->get('resource.page_cache'));
1193 $needle = trim($_POST['fromtag']); 1230 foreach ($alteredLinks as $link) {
1194 // True for case-sensitive tag search. 1231 $history->updateLink($link);
1195 $linksToAlter = $LINKSDB->filterSearch(array('searchtags' => $needle), true); 1232 }
1196 foreach($linksToAlter as $key=>$value) 1233 $delete = empty($_POST['totag']);
1197 { 1234 $redirect = $delete ? 'do=changetag' : 'searchtags='. urlencode(escape($_POST['totag']));
1198 $tags = explode(' ',trim($value['tags'])); 1235 $count = count($alteredLinks);
1199 unset($tags[array_search($needle,$tags)]); // Remove tag. 1236 $alert = $delete
1200 $value['tags']=trim(implode(' ',$tags)); 1237 ? sprintf(t('The tag was removed from %d link.', 'The tag was removed from %d links.', $count), $count)
1201 $LINKSDB[$key]=$value; 1238 : sprintf(t('The tag was renamed in %d link.', 'The tag was renamed in %d links.', $count), $count);
1202 } 1239 echo '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>';
1203 $LINKSDB->save($conf->get('resource.page_cache')); 1240 exit;
1204 echo '<script>alert("Tag was removed from '.count($linksToAlter).' links.");document.location=\'?\';</script>';
1205 exit;
1206 }
1207
1208 // Rename a tag:
1209 if (isset($_POST['renametag']) && !empty($_POST['fromtag']) && !empty($_POST['totag'])) {
1210 $needle = trim($_POST['fromtag']);
1211 // True for case-sensitive tag search.
1212 $linksToAlter = $LINKSDB->filterSearch(array('searchtags' => $needle), true);
1213 foreach($linksToAlter as $key=>$value)
1214 {
1215 $tags = explode(' ',trim($value['tags']));
1216 $tags[array_search($needle,$tags)] = trim($_POST['totag']); // Replace tags value.
1217 $value['tags']=trim(implode(' ',$tags));
1218 $LINKSDB[$key]=$value;
1219 }
1220 $LINKSDB->save($conf->get('resource.page_cache')); // Save to disk.
1221 echo '<script>alert("Tag was renamed in '.count($linksToAlter).' links.");document.location=\'?searchtags='.urlencode($_POST['totag']).'\';</script>';
1222 exit;
1223 }
1224 } 1241 }
1225 1242
1226 // -------- User wants to add a link without using the bookmarklet: Show form. 1243 // -------- User wants to add a link without using the bookmarklet: Show form.
@@ -1234,27 +1251,29 @@ function renderPage($conf, $pluginManager)
1234 if (isset($_POST['save_edit'])) 1251 if (isset($_POST['save_edit']))
1235 { 1252 {
1236 // Go away! 1253 // Go away!
1237 if (! tokenOk($_POST['token'])) { 1254 if (! $sessionManager->checkToken($_POST['token'])) {
1238 die('Wrong token.'); 1255 die(t('Wrong token.'));
1239 } 1256 }
1240 1257
1241 // lf_id should only be present if the link exists. 1258 // lf_id should only be present if the link exists.
1242 $id = !empty($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : $LINKSDB->getNextId(); 1259 $id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : $LINKSDB->getNextId();
1243 // Linkdate is kept here to: 1260 // Linkdate is kept here to:
1244 // - use the same permalink for notes as they're displayed when creating them 1261 // - use the same permalink for notes as they're displayed when creating them
1245 // - let users hack creation date of their posts 1262 // - let users hack creation date of their posts
1246 // See: https://github.com/shaarli/Shaarli/wiki/Datastore-hacks#changing-the-timestamp-for-a-link 1263 // See: https://shaarli.readthedocs.io/en/master/Various-hacks/#changing-the-timestamp-for-a-shaare
1247 $linkdate = escape($_POST['lf_linkdate']); 1264 $linkdate = escape($_POST['lf_linkdate']);
1248 if (isset($LINKSDB[$id])) { 1265 if (isset($LINKSDB[$id])) {
1249 // Edit 1266 // Edit
1250 $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate); 1267 $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate);
1251 $updated = new DateTime(); 1268 $updated = new DateTime();
1252 $shortUrl = $LINKSDB[$id]['shorturl']; 1269 $shortUrl = $LINKSDB[$id]['shorturl'];
1270 $new = false;
1253 } else { 1271 } else {
1254 // New link 1272 // New link
1255 $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate); 1273 $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate);
1256 $updated = null; 1274 $updated = null;
1257 $shortUrl = link_small_hash($created, $id); 1275 $shortUrl = link_small_hash($created, $id);
1276 $new = true;
1258 } 1277 }
1259 1278
1260 // Remove multiple spaces. 1279 // Remove multiple spaces.
@@ -1264,13 +1283,10 @@ function renderPage($conf, $pluginManager)
1264 // Remove duplicates. 1283 // Remove duplicates.
1265 $tags = implode(' ', array_unique(explode(' ', $tags))); 1284 $tags = implode(' ', array_unique(explode(' ', $tags)));
1266 1285
1267 $url = trim($_POST['lf_url']); 1286 if (empty(trim($_POST['lf_url']))) {
1268 if (! startsWith($url, 'http:') && ! startsWith($url, 'https:') 1287 $_POST['lf_url'] = '?' . smallHash($linkdate . $id);
1269 && ! startsWith($url, 'ftp:') && ! startsWith($url, 'magnet:')
1270 && ! startsWith($url, '?') && ! startsWith($url, 'javascript:')
1271 ) {
1272 $url = 'http://' . $url;
1273 } 1288 }
1289 $url = whitelist_protocols(trim($_POST['lf_url']), $conf->get('security.allowed_protocols'));
1274 1290
1275 $link = array( 1291 $link = array(
1276 'id' => $id, 1292 'id' => $id,
@@ -1293,7 +1309,11 @@ function renderPage($conf, $pluginManager)
1293 1309
1294 $LINKSDB[$id] = $link; 1310 $LINKSDB[$id] = $link;
1295 $LINKSDB->save($conf->get('resource.page_cache')); 1311 $LINKSDB->save($conf->get('resource.page_cache'));
1296 pubsubhub($conf); 1312 if ($new) {
1313 $history->addLink($link);
1314 } else {
1315 $history->updateLink($link);
1316 }
1297 1317
1298 // If we are called from the bookmarklet, we must close the popup: 1318 // If we are called from the bookmarklet, we must close the popup:
1299 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { 1319 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
@@ -1313,9 +1333,13 @@ function renderPage($conf, $pluginManager)
1313 // -------- User clicked the "Cancel" button when editing a link. 1333 // -------- User clicked the "Cancel" button when editing a link.
1314 if (isset($_POST['cancel_edit'])) 1334 if (isset($_POST['cancel_edit']))
1315 { 1335 {
1336 $id = isset($_POST['lf_id']) ? (int) escape($_POST['lf_id']) : false;
1337 if (! isset($LINKSDB[$id])) {
1338 header('Location: ?');
1339 }
1316 // If we are called from the bookmarklet, we must close the popup: 1340 // If we are called from the bookmarklet, we must close the popup:
1317 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo '<script>self.close();</script>'; exit; } 1341 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo '<script>self.close();</script>'; exit; }
1318 $link = $LINKSDB[(int) escape($_POST['lf_id'])]; 1342 $link = $LINKSDB[$id];
1319 $returnurl = ( isset($_POST['returnurl']) ? $_POST['returnurl'] : '?' ); 1343 $returnurl = ( isset($_POST['returnurl']) ? $_POST['returnurl'] : '?' );
1320 // Scroll to the link which has been edited. 1344 // Scroll to the link which has been edited.
1321 $returnurl .= '#'. $link['shorturl']; 1345 $returnurl .= '#'. $link['shorturl'];
@@ -1325,49 +1349,44 @@ function renderPage($conf, $pluginManager)
1325 } 1349 }
1326 1350
1327 // -------- User clicked the "Delete" button when editing a link: Delete link from database. 1351 // -------- User clicked the "Delete" button when editing a link: Delete link from database.
1328 if (isset($_POST['delete_link'])) 1352 if ($targetPage == Router::$PAGE_DELETELINK)
1329 { 1353 {
1330 if (!tokenOk($_POST['token'])) die('Wrong token.'); 1354 if (! $sessionManager->checkToken($_GET['token'])) {
1331 1355 die(t('Wrong token.'));
1332 // We do not need to ask for confirmation: 1356 }
1333 // - confirmation is handled by JavaScript
1334 // - we are protected from XSRF by the token.
1335
1336 // FIXME! We keep `lf_linkdate` for consistency before a proper API. To be removed.
1337 $id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : intval(escape($_POST['lf_linkdate']));
1338
1339 $pluginManager->executeHooks('delete_link', $LINKSDB[$id]);
1340 1357
1341 unset($LINKSDB[$id]); 1358 $ids = trim($_GET['lf_linkdate']);
1342 $LINKSDB->save('resource.page_cache'); // save to disk 1359 if (strpos($ids, ' ') !== false) {
1360 // multiple, space-separated ids provided
1361 $ids = array_values(array_filter(preg_split('/\s+/', escape($ids))));
1362 } else {
1363 // only a single id provided
1364 $ids = [$ids];
1365 }
1366 // assert at least one id is given
1367 if(!count($ids)){
1368 die('no id provided');
1369 }
1370 foreach ($ids as $id) {
1371 $id = (int) escape($id);
1372 $link = $LINKSDB[$id];
1373 $pluginManager->executeHooks('delete_link', $link);
1374 unset($LINKSDB[$id]);
1375 }
1376 $LINKSDB->save($conf->get('resource.page_cache')); // save to disk
1377 $history->deleteLink($link);
1343 1378
1344 // If we are called from the bookmarklet, we must close the popup: 1379 // If we are called from the bookmarklet, we must close the popup:
1345 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo '<script>self.close();</script>'; exit; } 1380 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo '<script>self.close();</script>'; exit; }
1346 // Pick where we're going to redirect 1381
1347 // ============================================================= 1382 $location = '?';
1348 // Basically, we can't redirect to where we were previously if it was a permalink 1383 if (isset($_SERVER['HTTP_REFERER'])) {
1349 // or an edit_link, because it would 404. 1384 // Don't redirect to where we were previously if it was a permalink or an edit_link, because it would 404.
1350 // Cases: 1385 $location = generateLocation(
1351 // - / : nothing in $_GET, redirect to self 1386 $_SERVER['HTTP_REFERER'],
1352 // - /?page : redirect to self 1387 $_SERVER['HTTP_HOST'],
1353 // - /?searchterm : redirect to self (there might be other links) 1388 ['delete_link', 'edit_link', $link['shorturl']]
1354 // - /?searchtags : redirect to self 1389 );
1355 // - /permalink : redirect to / (the link does not exist anymore)
1356 // - /?edit_link : redirect to / (the link does not exist anymore)
1357 // PHP treats the permalink as a $_GET variable, so we need to check if every condition for self
1358 // redirect is not satisfied, and only then redirect to /
1359 $location = "?";
1360 // Self redirection
1361 if (count($_GET) == 0
1362 || isset($_GET['page'])
1363 || isset($_GET['searchterm'])
1364 || isset($_GET['searchtags'])
1365 ) {
1366 if (isset($_POST['returnurl'])) {
1367 $location = $_POST['returnurl']; // Handle redirects given by the form
1368 } else {
1369 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('delete_link'));
1370 }
1371 } 1390 }
1372 1391
1373 header('Location: ' . $location); // After deleting the link, redirect to appropriate location 1392 header('Location: ' . $location); // After deleting the link, redirect to appropriate location
@@ -1385,7 +1404,7 @@ function renderPage($conf, $pluginManager)
1385 'link' => $link, 1404 'link' => $link,
1386 'link_is_new' => false, 1405 'link_is_new' => false,
1387 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''), 1406 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
1388 'tags' => $LINKSDB->allTags(), 1407 'tags' => $LINKSDB->linksCountPerTag(),
1389 ); 1408 );
1390 $pluginManager->executeHooks('render_editlink', $data); 1409 $pluginManager->executeHooks('render_editlink', $data);
1391 1410
@@ -1417,22 +1436,16 @@ function renderPage($conf, $pluginManager)
1417 // If this is an HTTP(S) link, we try go get the page to extract the title (otherwise we will to straight to the edit form.) 1436 // If this is an HTTP(S) link, we try go get the page to extract the title (otherwise we will to straight to the edit form.)
1418 if (empty($title) && strpos(get_url_scheme($url), 'http') !== false) { 1437 if (empty($title) && strpos(get_url_scheme($url), 'http') !== false) {
1419 // Short timeout to keep the application responsive 1438 // Short timeout to keep the application responsive
1420 list($headers, $content) = get_http_response($url, 4); 1439 // The callback will fill $charset and $title with data from the downloaded page.
1421 if (strpos($headers[0], '200 OK') !== false) { 1440 get_http_response($url, 25, 4194304, get_curl_download_callback($charset, $title));
1422 // Retrieve charset. 1441 if (! empty($title) && strtolower($charset) != 'utf-8') {
1423 $charset = get_charset($headers, $content); 1442 $title = mb_convert_encoding($title, 'utf-8', $charset);
1424 // Extract title.
1425 $title = html_extract_title($content);
1426 // Re-encode title in utf-8 if necessary.
1427 if (! empty($title) && strtolower($charset) != 'utf-8') {
1428 $title = mb_convert_encoding($title, 'utf-8', $charset);
1429 }
1430 } 1443 }
1431 } 1444 }
1432 1445
1433 if ($url == '') { 1446 if ($url == '') {
1434 $url = '?' . smallHash($linkdate . $LINKSDB->getNextId()); 1447 $url = '?' . smallHash($linkdate . $LINKSDB->getNextId());
1435 $title = 'Note: '; 1448 $title = $conf->get('general.default_note_title', t('Note: '));
1436 } 1449 }
1437 $url = escape($url); 1450 $url = escape($url);
1438 $title = escape($title); 1451 $title = escape($title);
@@ -1443,7 +1456,7 @@ function renderPage($conf, $pluginManager)
1443 'url' => $url, 1456 'url' => $url,
1444 'description' => $description, 1457 'description' => $description,
1445 'tags' => $tags, 1458 'tags' => $tags,
1446 'private' => $private 1459 'private' => $private,
1447 ); 1460 );
1448 } else { 1461 } else {
1449 $link['linkdate'] = $link['created']->format(LinkDB::LINK_DATE_FORMAT); 1462 $link['linkdate'] = $link['created']->format(LinkDB::LINK_DATE_FORMAT);
@@ -1454,7 +1467,7 @@ function renderPage($conf, $pluginManager)
1454 'link_is_new' => $link_is_new, 1467 'link_is_new' => $link_is_new,
1455 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''), 1468 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
1456 'source' => (isset($_GET['source']) ? $_GET['source'] : ''), 1469 'source' => (isset($_GET['source']) ? $_GET['source'] : ''),
1457 'tags' => $LINKSDB->allTags(), 1470 'tags' => $LINKSDB->linksCountPerTag(),
1458 'default_private_links' => $conf->get('privacy.default_private_links', false), 1471 'default_private_links' => $conf->get('privacy.default_private_links', false),
1459 ); 1472 );
1460 $pluginManager->executeHooks('render_editlink', $data); 1473 $pluginManager->executeHooks('render_editlink', $data);
@@ -1516,7 +1529,22 @@ function renderPage($conf, $pluginManager)
1516 1529
1517 if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) { 1530 if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) {
1518 // Show import dialog 1531 // Show import dialog
1519 $PAGE->assign('maxfilesize', getMaxFileSize()); 1532 $PAGE->assign(
1533 'maxfilesize',
1534 get_max_upload_size(
1535 ini_get('post_max_size'),
1536 ini_get('upload_max_filesize'),
1537 false
1538 )
1539 );
1540 $PAGE->assign(
1541 'maxfilesizeHuman',
1542 get_max_upload_size(
1543 ini_get('post_max_size'),
1544 ini_get('upload_max_filesize'),
1545 true
1546 )
1547 );
1520 $PAGE->renderPage('import'); 1548 $PAGE->renderPage('import');
1521 exit; 1549 exit;
1522 } 1550 }
@@ -1524,21 +1552,25 @@ function renderPage($conf, $pluginManager)
1524 // Import bookmarks from an uploaded file 1552 // Import bookmarks from an uploaded file
1525 if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) { 1553 if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) {
1526 // The file is too big or some form field may be missing. 1554 // The file is too big or some form field may be missing.
1527 echo '<script>alert("The file you are trying to upload is probably' 1555 $msg = sprintf(
1528 .' bigger than what this webserver can accept (' 1556 t(
1529 .getMaxFileSize().' bytes).' 1557 'The file you are trying to upload is probably bigger than what this webserver can accept'
1530 .' Please upload in smaller chunks.");document.location=\'?do=' 1558 .' (%s). Please upload in smaller chunks.'
1531 .Router::$PAGE_IMPORT .'\';</script>'; 1559 ),
1560 get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
1561 );
1562 echo '<script>alert("'. $msg .'");document.location=\'?do='.Router::$PAGE_IMPORT .'\';</script>';
1532 exit; 1563 exit;
1533 } 1564 }
1534 if (! tokenOk($_POST['token'])) { 1565 if (! $sessionManager->checkToken($_POST['token'])) {
1535 die('Wrong token.'); 1566 die('Wrong token.');
1536 } 1567 }
1537 $status = NetscapeBookmarkUtils::import( 1568 $status = NetscapeBookmarkUtils::import(
1538 $_POST, 1569 $_POST,
1539 $_FILES, 1570 $_FILES,
1540 $LINKSDB, 1571 $LINKSDB,
1541 $conf->get('resource.page_cache') 1572 $conf,
1573 $history
1542 ); 1574 );
1543 echo '<script>alert("'.$status.'");document.location=\'?do=' 1575 echo '<script>alert("'.$status.'");document.location=\'?do='
1544 .Router::$PAGE_IMPORT .'\';</script>'; 1576 .Router::$PAGE_IMPORT .'\';</script>';
@@ -1578,6 +1610,7 @@ function renderPage($conf, $pluginManager)
1578 $conf->set('general.enabled_plugins', save_plugin_config($_POST)); 1610 $conf->set('general.enabled_plugins', save_plugin_config($_POST));
1579 } 1611 }
1580 $conf->write(isLoggedIn()); 1612 $conf->write(isLoggedIn());
1613 $history->updateSettings();
1581 } 1614 }
1582 catch (Exception $e) { 1615 catch (Exception $e) {
1583 error_log( 1616 error_log(
@@ -1593,6 +1626,13 @@ function renderPage($conf, $pluginManager)
1593 exit; 1626 exit;
1594 } 1627 }
1595 1628
1629 // Get a fresh token
1630 if ($targetPage == Router::$GET_TOKEN) {
1631 header('Content-Type:text/plain');
1632 echo $sessionManager->generateToken($conf);
1633 exit;
1634 }
1635
1596 // -------- Otherwise, simply display search form and links: 1636 // -------- Otherwise, simply display search form and links:
1597 showLinkList($PAGE, $LINKSDB, $conf, $pluginManager); 1637 showLinkList($PAGE, $LINKSDB, $conf, $pluginManager);
1598 exit; 1638 exit;
@@ -1610,8 +1650,16 @@ function renderPage($conf, $pluginManager)
1610function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) 1650function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
1611{ 1651{
1612 // Used in templates 1652 // Used in templates
1613 $searchtags = !empty($_GET['searchtags']) ? escape($_GET['searchtags']) : ''; 1653 if (isset($_GET['searchtags'])) {
1614 $searchterm = !empty($_GET['searchterm']) ? escape($_GET['searchterm']) : ''; 1654 if (! empty($_GET['searchtags'])) {
1655 $searchtags = escape(normalize_spaces($_GET['searchtags']));
1656 } else {
1657 $searchtags = false;
1658 }
1659 } else {
1660 $searchtags = '';
1661 }
1662 $searchterm = !empty($_GET['searchterm']) ? escape(normalize_spaces($_GET['searchterm'])) : '';
1615 1663
1616 // Smallhash filter 1664 // Smallhash filter
1617 if (! empty($_SERVER['QUERY_STRING']) 1665 if (! empty($_SERVER['QUERY_STRING'])
@@ -1624,8 +1672,12 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
1624 } 1672 }
1625 } else { 1673 } else {
1626 // Filter links according search parameters. 1674 // Filter links according search parameters.
1627 $privateonly = !empty($_SESSION['privateonly']); 1675 $visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all';
1628 $linksToDisplay = $LINKSDB->filterSearch($_GET, false, $privateonly); 1676 $request = [
1677 'searchtags' => $searchtags,
1678 'searchterm' => $searchterm,
1679 ];
1680 $linksToDisplay = $LINKSDB->filterSearch($request, false, $visibility, !empty($_SESSION['untaggedonly']));
1629 } 1681 }
1630 1682
1631 // ---- Handle paging. 1683 // ---- Handle paging.
@@ -1649,7 +1701,11 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
1649 while ($i<$end && $i<count($keys)) 1701 while ($i<$end && $i<count($keys))
1650 { 1702 {
1651 $link = $linksToDisplay[$keys[$i]]; 1703 $link = $linksToDisplay[$keys[$i]];
1652 $link['description'] = format_description($link['description'], $conf->get('redirector.url')); 1704 $link['description'] = format_description(
1705 $link['description'],
1706 $conf->get('redirector.url'),
1707 $conf->get('redirector.encode_url')
1708 );
1653 $classLi = ($i % 2) != 0 ? '' : 'publicLinkHightLight'; 1709 $classLi = ($i % 2) != 0 ? '' : 'publicLinkHightLight';
1654 $link['class'] = $link['private'] == 0 ? $classLi : 'private'; 1710 $link['class'] = $link['private'] == 0 ? $classLi : 'private';
1655 $link['timestamp'] = $link['created']->getTimestamp(); 1711 $link['timestamp'] = $link['created']->getTimestamp();
@@ -1658,7 +1714,7 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
1658 } else { 1714 } else {
1659 $link['updated_timestamp'] = ''; 1715 $link['updated_timestamp'] = '';
1660 } 1716 }
1661 $taglist = explode(' ', $link['tags']); 1717 $taglist = preg_split('/\s+/', $link['tags'], -1, PREG_SPLIT_NO_EMPTY);
1662 uasort($taglist, 'strcasecmp'); 1718 uasort($taglist, 'strcasecmp');
1663 $link['taglist'] = $taglist; 1719 $link['taglist'] = $taglist;
1664 // Check for both signs of a note: starting with ? and 7 chars long. 1720 // Check for both signs of a note: starting with ? and 7 chars long.
@@ -1672,7 +1728,7 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
1672 } 1728 }
1673 1729
1674 // Compute paging navigation 1730 // Compute paging navigation
1675 $searchtagsUrl = empty($searchtags) ? '' : '&searchtags=' . urlencode($searchtags); 1731 $searchtagsUrl = $searchtags === '' ? '' : '&searchtags=' . urlencode($searchtags);
1676 $searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm); 1732 $searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm);
1677 $previous_page_url = ''; 1733 $previous_page_url = '';
1678 if ($i != count($keys)) { 1734 if ($i != count($keys)) {
@@ -1692,9 +1748,9 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
1692 'result_count' => count($linksToDisplay), 1748 'result_count' => count($linksToDisplay),
1693 'search_term' => $searchterm, 1749 'search_term' => $searchterm,
1694 'search_tags' => $searchtags, 1750 'search_tags' => $searchtags,
1751 'visibility' => ! empty($_SESSION['privateonly']) ? 'private' : '',
1695 'redirector' => $conf->get('redirector.url'), // Optional redirector URL. 1752 'redirector' => $conf->get('redirector.url'), // Optional redirector URL.
1696 'links' => $linkDisp, 1753 'links' => $linkDisp,
1697 'tags' => $LINKSDB->allTags(),
1698 ); 1754 );
1699 1755
1700 // If there is only a single link, we change on-the-fly the title of the page. 1756 // If there is only a single link, we change on-the-fly the title of the page.
@@ -1903,10 +1959,10 @@ function lazyThumbnail($conf, $url,$href=false)
1903 * Installation 1959 * Installation
1904 * This function should NEVER be called if the file data/config.php exists. 1960 * This function should NEVER be called if the file data/config.php exists.
1905 * 1961 *
1906 * @param ConfigManager $conf Configuration Manager instance. 1962 * @param ConfigManager $conf Configuration Manager instance.
1963 * @param SessionManager $sessionManager SessionManager instance
1907 */ 1964 */
1908function install($conf) 1965function install($conf, $sessionManager) {
1909{
1910 // On free.fr host, make sure the /sessions directory exists, otherwise login will not work. 1966 // On free.fr host, make sure the /sessions directory exists, otherwise login will not work.
1911 if (endsWith($_SERVER['HTTP_HOST'],'.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions',0705); 1967 if (endsWith($_SERVER['HTTP_HOST'],'.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions',0705);
1912 1968
@@ -1915,12 +1971,20 @@ function install($conf)
1915 // (Because on some hosts, session.save_path may not be set correctly, 1971 // (Because on some hosts, session.save_path may not be set correctly,
1916 // or we may not have write access to it.) 1972 // or we may not have write access to it.)
1917 if (isset($_GET['test_session']) && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working')) 1973 if (isset($_GET['test_session']) && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working'))
1918 { // Step 2: Check if data in session is correct. 1974 {
1919 echo '<pre>Sessions do not seem to work correctly on your server.<br>'; 1975 // Step 2: Check if data in session is correct.
1920 echo 'Make sure the variable session.save_path is set correctly in your php config, and that you have write access to it.<br>'; 1976 $msg = t(
1921 echo 'It currently points to '.session_save_path().'<br>'; 1977 '<pre>Sessions do not seem to work correctly on your server.<br>'.
1922 echo 'Check that the hostname used to access Shaarli contains a dot. On some browsers, accessing your server via a hostname like \'localhost\' or any custom hostname without a dot causes cookie storage to fail. We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'; 1978 'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
1923 echo '<br><a href="?">Click to try again.</a></pre>'; 1979 'and that you have write access to it.<br>'.
1980 'It currently points to %s.<br>'.
1981 'On some browsers, accessing your server via a hostname like \'localhost\' '.
1982 'or any custom hostname without a dot causes cookie storage to fail. '.
1983 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'
1984 );
1985 $msg = sprintf($msg, session_save_path());
1986 echo $msg;
1987 echo '<br><a href="?">'. t('Click to try again.') .'</a></pre>';
1924 die; 1988 die;
1925 } 1989 }
1926 if (!isset($_SESSION['session_tested'])) 1990 if (!isset($_SESSION['session_tested']))
@@ -1953,7 +2017,16 @@ function install($conf)
1953 } else { 2017 } else {
1954 $conf->set('general.title', 'Shared links on '.escape(index_url($_SERVER))); 2018 $conf->set('general.title', 'Shared links on '.escape(index_url($_SERVER)));
1955 } 2019 }
2020 $conf->set('translation.language', escape($_POST['language']));
1956 $conf->set('updates.check_updates', !empty($_POST['updateCheck'])); 2021 $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
2022 $conf->set('api.enabled', !empty($_POST['enableApi']));
2023 $conf->set(
2024 'api.secret',
2025 generate_api_secret(
2026 $conf->get('credentials.login'),
2027 $conf->get('credentials.salt')
2028 )
2029 );
1957 try { 2030 try {
1958 // Everything is ok, let's create config file. 2031 // Everything is ok, let's create config file.
1959 $conf->write(isLoggedIn()); 2032 $conf->write(isLoggedIn());
@@ -1972,16 +2045,11 @@ function install($conf)
1972 exit; 2045 exit;
1973 } 2046 }
1974 2047
1975 // Display config form: 2048 $PAGE = new PageBuilder($conf, null, $sessionManager->generateToken());
1976 list($timezone_form, $timezone_js) = generateTimeZoneForm(); 2049 list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
1977 $timezone_html = ''; 2050 $PAGE->assign('continents', $continents);
1978 if ($timezone_form != '') { 2051 $PAGE->assign('cities', $cities);
1979 $timezone_html = '<tr><td><b>Timezone:</b></td><td>'.$timezone_form.'</td></tr>'; 2052 $PAGE->assign('languages', Languages::getAvailableLanguages());
1980 }
1981
1982 $PAGE = new PageBuilder($conf);
1983 $PAGE->assign('timezone_html',$timezone_html);
1984 $PAGE->assign('timezone_js',$timezone_js);
1985 $PAGE->renderPage('install'); 2053 $PAGE->renderPage('install');
1986 exit; 2054 exit;
1987} 2055}
@@ -2216,4 +2284,45 @@ if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=
2216if (!isset($_SESSION['LINKS_PER_PAGE'])) { 2284if (!isset($_SESSION['LINKS_PER_PAGE'])) {
2217 $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20); 2285 $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20);
2218} 2286}
2219renderPage($conf, $pluginManager); 2287
2288try {
2289 $history = new History($conf->get('resource.history'));
2290} catch(Exception $e) {
2291 die($e->getMessage());
2292}
2293
2294$linkDb = new LinkDB(
2295 $conf->get('resource.datastore'),
2296 isLoggedIn(),
2297 $conf->get('privacy.hide_public_links'),
2298 $conf->get('redirector.url'),
2299 $conf->get('redirector.encode_url')
2300);
2301
2302$container = new \Slim\Container();
2303$container['conf'] = $conf;
2304$container['plugins'] = $pluginManager;
2305$container['history'] = $history;
2306$app = new \Slim\App($container);
2307
2308// REST API routes
2309$app->group('/api/v1', function() {
2310 $this->get('/info', '\Shaarli\Api\Controllers\Info:getInfo')->setName('getInfo');
2311 $this->get('/links', '\Shaarli\Api\Controllers\Links:getLinks')->setName('getLinks');
2312 $this->get('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:getLink')->setName('getLink');
2313 $this->post('/links', '\Shaarli\Api\Controllers\Links:postLink')->setName('postLink');
2314 $this->put('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:putLink')->setName('putLink');
2315 $this->delete('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:deleteLink')->setName('deleteLink');
2316 $this->get('/history', '\Shaarli\Api\Controllers\History:getHistory')->setName('getHistory');
2317})->add('\Shaarli\Api\ApiMiddleware');
2318
2319$response = $app->run(true);
2320// Hack to make Slim and Shaarli router work together:
2321// If a Slim route isn't found and NOT API call, we call renderPage().
2322if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) {
2323 // We use UTF-8 for proper international characters handling.
2324 header('Content-Type: text/html; charset=utf-8');
2325 renderPage($conf, $pluginManager, $linkDb, $history, $sessionManager);
2326} else {
2327 $app->respond($response);
2328}