aboutsummaryrefslogtreecommitdiffhomepage
path: root/index.php
diff options
context:
space:
mode:
Diffstat (limited to 'index.php')
-rw-r--r--index.php982
1 files changed, 263 insertions, 719 deletions
diff --git a/index.php b/index.php
index 2de9be0a..1480bbc5 100644
--- a/index.php
+++ b/index.php
@@ -48,7 +48,7 @@ if (! file_exists(__DIR__ . '/vendor/autoload.php')) {
48 ."If you installed Shaarli through Git or using the development branch,\n" 48 ."If you installed Shaarli through Git or using the development branch,\n"
49 ."please refer to the installation documentation to install PHP" 49 ."please refer to the installation documentation to install PHP"
50 ." dependencies using Composer:\n" 50 ." dependencies using Composer:\n"
51 ."- https://shaarli.readthedocs.io/en/master/Server-requirements/\n" 51 ."- https://shaarli.readthedocs.io/en/master/Server-configuration/\n"
52 ."- https://shaarli.readthedocs.io/en/master/Download-and-Installation/"; 52 ."- https://shaarli.readthedocs.io/en/master/Download-and-Installation/";
53 exit; 53 exit;
54} 54}
@@ -75,10 +75,12 @@ require_once 'application/Utils.php';
75require_once 'application/PluginManager.php'; 75require_once 'application/PluginManager.php';
76require_once 'application/Router.php'; 76require_once 'application/Router.php';
77require_once 'application/Updater.php'; 77require_once 'application/Updater.php';
78use \Shaarli\Config\ConfigManager;
78use \Shaarli\Languages; 79use \Shaarli\Languages;
80use \Shaarli\Security\LoginManager;
81use \Shaarli\Security\SessionManager;
79use \Shaarli\ThemeUtils; 82use \Shaarli\ThemeUtils;
80use \Shaarli\Config\ConfigManager; 83use \Shaarli\Thumbnailer;
81use \Shaarli\SessionManager;
82 84
83// Ensure the PHP version is supported 85// Ensure the PHP version is supported
84try { 86try {
@@ -100,8 +102,6 @@ if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
100// Set default cookie expiration and path. 102// Set default cookie expiration and path.
101session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']); 103session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']);
102// Set session parameters on server side. 104// Set session parameters on server side.
103// If the user does not access any page within this time, his/her session is considered expired.
104define('INACTIVITY_TIMEOUT', 3600); // in seconds.
105// Use cookies to store session. 105// Use cookies to store session.
106ini_set('session.use_cookies', 1); 106ini_set('session.use_cookies', 1);
107// Force cookies for session (phpsessionID forbidden in URL). 107// Force cookies for session (phpsessionID forbidden in URL).
@@ -123,6 +123,9 @@ if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli']))
123 123
124$conf = new ConfigManager(); 124$conf = new ConfigManager();
125$sessionManager = new SessionManager($_SESSION, $conf); 125$sessionManager = new SessionManager($_SESSION, $conf);
126$loginManager = new LoginManager($GLOBALS, $conf, $sessionManager);
127$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
128$clientIpId = client_ip_id($_SERVER);
126 129
127// LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead. 130// LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead.
128if (! defined('LC_MESSAGES')) { 131if (! defined('LC_MESSAGES')) {
@@ -172,246 +175,64 @@ if (! is_file($conf->getConfigFileExt())) {
172 } 175 }
173 176
174 // Display the installation form if no existing config is found 177 // Display the installation form if no existing config is found
175 install($conf, $sessionManager); 178 install($conf, $sessionManager, $loginManager);
176} 179}
177 180
178// a token depending of deployment salt, user password, and the current ip 181$loginManager->checkLoginState($_COOKIE, $clientIpId);
179define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt')));
180 182
181/** 183/**
182 * Checking session state (i.e. is the user still logged in) 184 * Adapter function to ensure compatibility with third-party templates
183 * 185 *
184 * @param ConfigManager $conf The configuration manager. 186 * @see https://github.com/shaarli/Shaarli/pull/1086
185 * 187 *
186 * @return bool: true if the user is logged in, false otherwise. 188 * @return bool true when the user is logged in, false otherwise
187 */ 189 */
188function setup_login_state($conf)
189{
190 if ($conf->get('security.open_shaarli')) {
191 return true;
192 }
193 $userIsLoggedIn = false; // By default, we do not consider the user as logged in;
194 $loginFailure = false; // If set to true, every attempt to authenticate the user will fail. This indicates that an important condition isn't met.
195 if (! $conf->exists('credentials.login')) {
196 $userIsLoggedIn = false; // Shaarli is not configured yet.
197 $loginFailure = true;
198 }
199 if (isset($_COOKIE['shaarli_staySignedIn']) &&
200 $_COOKIE['shaarli_staySignedIn']===STAY_SIGNED_IN_TOKEN &&
201 !$loginFailure)
202 {
203 fillSessionInfo($conf);
204 $userIsLoggedIn = true;
205 }
206 // If session does not exist on server side, or IP address has changed, or session has expired, logout.
207 if (empty($_SESSION['uid'])
208 || ($conf->get('security.session_protection_disabled') === false && $_SESSION['ip'] != allIPs())
209 || time() >= $_SESSION['expires_on'])
210 {
211 logout();
212 $userIsLoggedIn = false;
213 $loginFailure = true;
214 }
215 if (!empty($_SESSION['longlastingsession'])) {
216 $_SESSION['expires_on']=time()+$_SESSION['longlastingsession']; // In case of "Stay signed in" checked.
217 }
218 else {
219 $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Standard session expiration date.
220 }
221 if (!$loginFailure) {
222 $userIsLoggedIn = true;
223 }
224
225 return $userIsLoggedIn;
226}
227$userIsLoggedIn = setup_login_state($conf);
228
229// ------------------------------------------------------------------------------------------
230// Session management
231
232// Returns the IP address of the client (Used to prevent session cookie hijacking.)
233function allIPs()
234{
235 $ip = $_SERVER['REMOTE_ADDR'];
236 // Then we use more HTTP headers to prevent session hijacking from users behind the same proxy.
237 if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $ip=$ip.'_'.$_SERVER['HTTP_X_FORWARDED_FOR']; }
238 if (isset($_SERVER['HTTP_CLIENT_IP'])) { $ip=$ip.'_'.$_SERVER['HTTP_CLIENT_IP']; }
239 return $ip;
240}
241
242/**
243 * Load user session.
244 *
245 * @param ConfigManager $conf Configuration Manager instance.
246 */
247function fillSessionInfo($conf)
248{
249 $_SESSION['uid'] = sha1(uniqid('',true).'_'.mt_rand()); // Generate unique random number (different than phpsessionid)
250 $_SESSION['ip']=allIPs(); // We store IP address(es) of the client to make sure session is not hijacked.
251 $_SESSION['username']= $conf->get('credentials.login');
252 $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Set session expiration.
253}
254
255/**
256 * Check that user/password is correct.
257 *
258 * @param string $login Username
259 * @param string $password User password
260 * @param ConfigManager $conf Configuration Manager instance.
261 *
262 * @return bool: authentication successful or not.
263 */
264function check_auth($login, $password, $conf)
265{
266 $hash = sha1($password . $login . $conf->get('credentials.salt'));
267 if ($login == $conf->get('credentials.login') && $hash == $conf->get('credentials.hash'))
268 { // Login/password is correct.
269 fillSessionInfo($conf);
270 logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login successful');
271 return true;
272 }
273 logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login failed for user '.$login);
274 return false;
275}
276
277// Returns true if the user is logged in.
278function isLoggedIn() 190function isLoggedIn()
279{ 191{
280 global $userIsLoggedIn; 192 global $loginManager;
281 return $userIsLoggedIn; 193 return $loginManager->isLoggedIn();
282}
283
284// Force logout.
285function logout() {
286 if (isset($_SESSION)) {
287 unset($_SESSION['uid']);
288 unset($_SESSION['ip']);
289 unset($_SESSION['username']);
290 unset($_SESSION['privateonly']);
291 unset($_SESSION['untaggedonly']);
292 }
293 setcookie('shaarli_staySignedIn', FALSE, 0, WEB_PATH);
294} 194}
295 195
296 196
297// ------------------------------------------------------------------------------------------ 197// ------------------------------------------------------------------------------------------
298// Brute force protection system
299// Several consecutive failed logins will ban the IP address for 30 minutes.
300if (!is_file($conf->get('resource.ban_file', 'data/ipbans.php'))) {
301 // FIXME! globals
302 file_put_contents(
303 $conf->get('resource.ban_file', 'data/ipbans.php'),
304 "<?php\n\$GLOBALS['IPBANS']=".var_export(array('FAILURES'=>array(),'BANS'=>array()),true).";\n?>"
305 );
306}
307include $conf->get('resource.ban_file', 'data/ipbans.php');
308/**
309 * Signal a failed login. Will ban the IP if too many failures:
310 *
311 * @param ConfigManager $conf Configuration Manager instance.
312 */
313function ban_loginFailed($conf)
314{
315 $ip = $_SERVER['REMOTE_ADDR'];
316 $trusted = $conf->get('security.trusted_proxies', array());
317 if (in_array($ip, $trusted)) {
318 $ip = getIpAddressFromProxy($_SERVER, $trusted);
319 if (!$ip) {
320 return;
321 }
322 }
323 $gb = $GLOBALS['IPBANS'];
324 if (! isset($gb['FAILURES'][$ip])) {
325 $gb['FAILURES'][$ip]=0;
326 }
327 $gb['FAILURES'][$ip]++;
328 if ($gb['FAILURES'][$ip] > ($conf->get('security.ban_after') - 1))
329 {
330 $gb['BANS'][$ip] = time() + $conf->get('security.ban_after', 1800);
331 logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'IP address banned from login');
332 }
333 $GLOBALS['IPBANS'] = $gb;
334 file_put_contents(
335 $conf->get('resource.ban_file', 'data/ipbans.php'),
336 "<?php\n\$GLOBALS['IPBANS']=".var_export($gb,true).";\n?>"
337 );
338}
339
340/**
341 * Signals a successful login. Resets failed login counter.
342 *
343 * @param ConfigManager $conf Configuration Manager instance.
344 */
345function ban_loginOk($conf)
346{
347 $ip = $_SERVER['REMOTE_ADDR'];
348 $gb = $GLOBALS['IPBANS'];
349 unset($gb['FAILURES'][$ip]); unset($gb['BANS'][$ip]);
350 $GLOBALS['IPBANS'] = $gb;
351 file_put_contents(
352 $conf->get('resource.ban_file', 'data/ipbans.php'),
353 "<?php\n\$GLOBALS['IPBANS']=".var_export($gb,true).";\n?>"
354 );
355}
356
357/**
358 * Checks if the user CAN login. If 'true', the user can try to login.
359 *
360 * @param ConfigManager $conf Configuration Manager instance.
361 *
362 * @return bool: true if the user is allowed to login.
363 */
364function ban_canLogin($conf)
365{
366 $ip=$_SERVER["REMOTE_ADDR"]; $gb=$GLOBALS['IPBANS'];
367 if (isset($gb['BANS'][$ip]))
368 {
369 // User is banned. Check if the ban has expired:
370 if ($gb['BANS'][$ip]<=time())
371 { // Ban expired, user can try to login again.
372 logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Ban lifted.');
373 unset($gb['FAILURES'][$ip]); unset($gb['BANS'][$ip]);
374 file_put_contents(
375 $conf->get('resource.ban_file', 'data/ipbans.php'),
376 "<?php\n\$GLOBALS['IPBANS']=".var_export($gb,true).";\n?>"
377 );
378 return true; // Ban has expired, user can login.
379 }
380 return false; // User is banned.
381 }
382 return true; // User is not banned.
383}
384
385// ------------------------------------------------------------------------------------------
386// Process login form: Check if login/password is correct. 198// Process login form: Check if login/password is correct.
387if (isset($_POST['login'])) 199if (isset($_POST['login'])) {
388{ 200 if (! $loginManager->canLogin($_SERVER)) {
389 if (!ban_canLogin($conf)) die(t('I said: NO. You are banned for the moment. Go away.')); 201 die(t('I said: NO. You are banned for the moment. Go away.'));
202 }
390 if (isset($_POST['password']) 203 if (isset($_POST['password'])
391 && $sessionManager->checkToken($_POST['token']) 204 && $sessionManager->checkToken($_POST['token'])
392 && (check_auth($_POST['login'], $_POST['password'], $conf)) 205 && $loginManager->checkCredentials($_SERVER['REMOTE_ADDR'], $clientIpId, $_POST['login'], $_POST['password'])
393 ) { // Login/password is OK. 206 ) {
394 ban_loginOk($conf); 207 $loginManager->handleSuccessfulLogin($_SERVER);
395 // If user wants to keep the session cookie even after the browser closes:
396 if (!empty($_POST['longlastingsession']))
397 {
398 $_SESSION['longlastingsession'] = 31536000; // (31536000 seconds = 1 year)
399 $expiration = time() + $_SESSION['longlastingsession']; // calculate relative cookie expiration (1 year from now)
400 setcookie('shaarli_staySignedIn', STAY_SIGNED_IN_TOKEN, $expiration, WEB_PATH);
401 $_SESSION['expires_on'] = $expiration; // Set session expiration on server-side.
402 208
403 $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/'; 209 $cookiedir = '';
404 session_set_cookie_params($_SESSION['longlastingsession'],$cookiedir,$_SERVER['SERVER_NAME']); // Set session cookie expiration on client side 210 if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
405 // Note: Never forget the trailing slash on the cookie path! 211 // Note: Never forget the trailing slash on the cookie path!
406 session_regenerate_id(true); // Send cookie with new expiration date to browser. 212 $cookiedir = dirname($_SERVER["SCRIPT_NAME"]) . '/';
407 } 213 }
408 else // Standard session expiration (=when browser closes) 214
409 { 215 if (!empty($_POST['longlastingsession'])) {
410 $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/'; 216 // Keep the session cookie even after the browser closes
411 session_set_cookie_params(0,$cookiedir,$_SERVER['SERVER_NAME']); // 0 means "When browser closes" 217 $sessionManager->setStaySignedIn(true);
412 session_regenerate_id(true); 218 $expirationTime = $sessionManager->extendSession();
219
220 setcookie(
221 $loginManager::$STAY_SIGNED_IN_COOKIE,
222 $loginManager->getStaySignedInToken(),
223 $expirationTime,
224 WEB_PATH
225 );
226
227 } else {
228 // Standard session expiration (=when browser closes)
229 $expirationTime = 0;
413 } 230 }
414 231
232 // Send cookie with the new expiration date to the browser
233 session_set_cookie_params($expirationTime, $cookiedir, $_SERVER['SERVER_NAME']);
234 session_regenerate_id(true);
235
415 // Optional redirect after login: 236 // Optional redirect after login:
416 if (isset($_GET['post'])) { 237 if (isset($_GET['post'])) {
417 $uri = '?post='. urlencode($_GET['post']); 238 $uri = '?post='. urlencode($_GET['post']);
@@ -437,10 +258,8 @@ if (isset($_POST['login']))
437 } 258 }
438 } 259 }
439 header('Location: ?'); exit; 260 header('Location: ?'); exit;
440 } 261 } else {
441 else 262 $loginManager->handleFailedLogin($_SERVER);
442 {
443 ban_loginFailed($conf);
444 $redir = '&username='. urlencode($_POST['login']); 263 $redir = '&username='. urlencode($_POST['login']);
445 if (isset($_GET['post'])) { 264 if (isset($_GET['post'])) {
446 $redir .= '&post=' . urlencode($_GET['post']); 265 $redir .= '&post=' . urlencode($_GET['post']);
@@ -466,15 +285,16 @@ if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are atta
466 * Gives the last 7 days (which have links). 285 * Gives the last 7 days (which have links).
467 * This RSS feed cannot be filtered. 286 * This RSS feed cannot be filtered.
468 * 287 *
469 * @param ConfigManager $conf Configuration Manager instance. 288 * @param ConfigManager $conf Configuration Manager instance
289 * @param LoginManager $loginManager LoginManager instance
470 */ 290 */
471function showDailyRSS($conf) { 291function showDailyRSS($conf, $loginManager) {
472 // Cache system 292 // Cache system
473 $query = $_SERVER['QUERY_STRING']; 293 $query = $_SERVER['QUERY_STRING'];
474 $cache = new CachedPage( 294 $cache = new CachedPage(
475 $conf->get('config.PAGE_CACHE'), 295 $conf->get('config.PAGE_CACHE'),
476 page_url($_SERVER), 296 page_url($_SERVER),
477 startsWith($query,'do=dailyrss') && !isLoggedIn() 297 startsWith($query,'do=dailyrss') && !$loginManager->isLoggedIn()
478 ); 298 );
479 $cached = $cache->cachedVersion(); 299 $cached = $cache->cachedVersion();
480 if (!empty($cached)) { 300 if (!empty($cached)) {
@@ -486,7 +306,7 @@ function showDailyRSS($conf) {
486 // Read links from database (and filter private links if used it not logged in). 306 // Read links from database (and filter private links if used it not logged in).
487 $LINKSDB = new LinkDB( 307 $LINKSDB = new LinkDB(
488 $conf->get('resource.datastore'), 308 $conf->get('resource.datastore'),
489 isLoggedIn(), 309 $loginManager->isLoggedIn(),
490 $conf->get('privacy.hide_public_links'), 310 $conf->get('privacy.hide_public_links'),
491 $conf->get('redirector.url'), 311 $conf->get('redirector.url'),
492 $conf->get('redirector.encode_url') 312 $conf->get('redirector.encode_url')
@@ -568,9 +388,10 @@ function showDailyRSS($conf) {
568 * @param PageBuilder $pageBuilder Template engine wrapper. 388 * @param PageBuilder $pageBuilder Template engine wrapper.
569 * @param LinkDB $LINKSDB LinkDB instance. 389 * @param LinkDB $LINKSDB LinkDB instance.
570 * @param ConfigManager $conf Configuration Manager instance. 390 * @param ConfigManager $conf Configuration Manager instance.
571 * @param PluginManager $pluginManager Plugin Manager instane. 391 * @param PluginManager $pluginManager Plugin Manager instance.
392 * @param LoginManager $loginManager Login Manager instance
572 */ 393 */
573function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) 394function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager)
574{ 395{
575 $day = date('Ymd', strtotime('-1 day')); // Yesterday, in format YYYYMMDD. 396 $day = date('Ymd', strtotime('-1 day')); // Yesterday, in format YYYYMMDD.
576 if (isset($_GET['day'])) { 397 if (isset($_GET['day'])) {
@@ -616,6 +437,20 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
616 $linksToDisplay[$key]['timestamp'] = $link['created']->getTimestamp(); 437 $linksToDisplay[$key]['timestamp'] = $link['created']->getTimestamp();
617 } 438 }
618 439
440 $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000');
441 $data = array(
442 'pagetitle' => $conf->get('general.title') .' - '. format_date($dayDate, false),
443 'linksToDisplay' => $linksToDisplay,
444 'day' => $dayDate->getTimestamp(),
445 'dayDate' => $dayDate,
446 'previousday' => $previousday,
447 'nextday' => $nextday,
448 );
449
450 /* Hook is called before column construction so that plugins don't have
451 to deal with columns. */
452 $pluginManager->executeHooks('render_daily', $data, array('loggedin' => $loginManager->isLoggedIn()));
453
619 /* We need to spread the articles on 3 columns. 454 /* We need to spread the articles on 3 columns.
620 I did not want to use a JavaScript lib like http://masonry.desandro.com/ 455 I did not want to use a JavaScript lib like http://masonry.desandro.com/
621 so I manually spread entries with a simple method: I roughly evaluate the 456 so I manually spread entries with a simple method: I roughly evaluate the
@@ -623,7 +458,7 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
623 */ 458 */
624 $columns = array(array(), array(), array()); // Entries to display, for each column. 459 $columns = array(array(), array(), array()); // Entries to display, for each column.
625 $fill = array(0, 0, 0); // Rough estimate of columns fill. 460 $fill = array(0, 0, 0); // Rough estimate of columns fill.
626 foreach($linksToDisplay as $key => $link) { 461 foreach($data['linksToDisplay'] as $key => $link) {
627 // Roughly estimate length of entry (by counting characters) 462 // Roughly estimate length of entry (by counting characters)
628 // Title: 30 chars = 1 line. 1 line is 30 pixels height. 463 // Title: 30 chars = 1 line. 1 line is 30 pixels height.
629 // Description: 836 characters gives roughly 342 pixel height. 464 // Description: 836 characters gives roughly 342 pixel height.
@@ -639,23 +474,13 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
639 $fill[$index] += $length; 474 $fill[$index] += $length;
640 } 475 }
641 476
642 $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000'); 477 $data['cols'] = $columns;
643 $data = array(
644 'pagetitle' => $conf->get('general.title') .' - '. format_date($dayDate, false),
645 'linksToDisplay' => $linksToDisplay,
646 'cols' => $columns,
647 'day' => $dayDate->getTimestamp(),
648 'dayDate' => $dayDate,
649 'previousday' => $previousday,
650 'nextday' => $nextday,
651 );
652
653 $pluginManager->executeHooks('render_daily', $data, array('loggedin' => isLoggedIn()));
654 478
655 foreach ($data as $key => $value) { 479 foreach ($data as $key => $value) {
656 $pageBuilder->assign($key, $value); 480 $pageBuilder->assign($key, $value);
657 } 481 }
658 482
483 $pageBuilder->assign('pagetitle', t('Daily') .' - '. $conf->get('general.title', 'Shaarli'));
659 $pageBuilder->renderPage('daily'); 484 $pageBuilder->renderPage('daily');
660 exit; 485 exit;
661} 486}
@@ -668,8 +493,8 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
668 * @param ConfigManager $conf Configuration Manager instance. 493 * @param ConfigManager $conf Configuration Manager instance.
669 * @param PluginManager $pluginManager Plugin Manager instance. 494 * @param PluginManager $pluginManager Plugin Manager instance.
670 */ 495 */
671function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager) { 496function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager) {
672 buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager); // Compute list of links to display 497 buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager, $loginManager);
673 $PAGE->renderPage('linklist'); 498 $PAGE->renderPage('linklist');
674} 499}
675 500
@@ -681,14 +506,16 @@ function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager) {
681 * @param LinkDB $LINKSDB 506 * @param LinkDB $LINKSDB
682 * @param History $history instance 507 * @param History $history instance
683 * @param SessionManager $sessionManager SessionManager instance 508 * @param SessionManager $sessionManager SessionManager instance
509 * @param LoginManager $loginManager LoginManager instance
684 */ 510 */
685function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager) 511function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, $loginManager)
686{ 512{
687 $updater = new Updater( 513 $updater = new Updater(
688 read_updates_file($conf->get('resource.updates')), 514 read_updates_file($conf->get('resource.updates')),
689 $LINKSDB, 515 $LINKSDB,
690 $conf, 516 $conf,
691 isLoggedIn() 517 $loginManager->isLoggedIn(),
518 $_SESSION
692 ); 519 );
693 try { 520 try {
694 $newUpdates = $updater->update(); 521 $newUpdates = $updater->update();
@@ -703,18 +530,18 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
703 die($e->getMessage()); 530 die($e->getMessage());
704 } 531 }
705 532
706 $PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken()); 533 $PAGE = new PageBuilder($conf, $_SESSION, $LINKSDB, $sessionManager->generateToken(), $loginManager->isLoggedIn());
707 $PAGE->assign('linkcount', count($LINKSDB)); 534 $PAGE->assign('linkcount', count($LINKSDB));
708 $PAGE->assign('privateLinkcount', count_private($LINKSDB)); 535 $PAGE->assign('privateLinkcount', count_private($LINKSDB));
709 $PAGE->assign('plugin_errors', $pluginManager->getErrors()); 536 $PAGE->assign('plugin_errors', $pluginManager->getErrors());
710 537
711 // Determine which page will be rendered. 538 // Determine which page will be rendered.
712 $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : ''; 539 $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : '';
713 $targetPage = Router::findPage($query, $_GET, isLoggedIn()); 540 $targetPage = Router::findPage($query, $_GET, $loginManager->isLoggedIn());
714 541
715 if ( 542 if (
716 // if the user isn't logged in 543 // if the user isn't logged in
717 !isLoggedIn() && 544 !$loginManager->isLoggedIn() &&
718 // and Shaarli doesn't have public content... 545 // and Shaarli doesn't have public content...
719 $conf->get('privacy.hide_public_links') && 546 $conf->get('privacy.hide_public_links') &&
720 // and is configured to enforce the login 547 // and is configured to enforce the login
@@ -742,7 +569,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
742 $pluginManager->executeHooks('render_' . $name, $plugin_data, 569 $pluginManager->executeHooks('render_' . $name, $plugin_data,
743 array( 570 array(
744 'target' => $targetPage, 571 'target' => $targetPage,
745 'loggedin' => isLoggedIn() 572 'loggedin' => $loginManager->isLoggedIn()
746 ) 573 )
747 ); 574 );
748 $PAGE->assign('plugins_' . $name, $plugin_data); 575 $PAGE->assign('plugins_' . $name, $plugin_data);
@@ -758,6 +585,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
758 $PAGE->assign('returnurl',(isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']):'')); 585 $PAGE->assign('returnurl',(isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']):''));
759 // add default state of the 'remember me' checkbox 586 // add default state of the 'remember me' checkbox
760 $PAGE->assign('remember_user_default', $conf->get('privacy.remember_user_default')); 587 $PAGE->assign('remember_user_default', $conf->get('privacy.remember_user_default'));
588 $PAGE->assign('user_can_login', $loginManager->canLogin($_SERVER));
589 $PAGE->assign('pagetitle', t('Login') .' - '. $conf->get('general.title', 'Shaarli'));
761 $PAGE->renderPage('loginform'); 590 $PAGE->renderPage('loginform');
762 exit; 591 exit;
763 } 592 }
@@ -765,7 +594,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
765 if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=logout')) 594 if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=logout'))
766 { 595 {
767 invalidateCaches($conf->get('resource.page_cache')); 596 invalidateCaches($conf->get('resource.page_cache'));
768 logout(); 597 $sessionManager->logout();
598 setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, WEB_PATH);
769 header('Location: ?'); 599 header('Location: ?');
770 exit; 600 exit;
771 } 601 }
@@ -773,31 +603,36 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
773 // -------- Picture wall 603 // -------- Picture wall
774 if ($targetPage == Router::$PAGE_PICWALL) 604 if ($targetPage == Router::$PAGE_PICWALL)
775 { 605 {
606 $PAGE->assign('pagetitle', t('Picture wall') .' - '. $conf->get('general.title', 'Shaarli'));
607 if (! $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) === Thumbnailer::MODE_NONE) {
608 $PAGE->assign('linksToDisplay', []);
609 $PAGE->renderPage('picwall');
610 exit;
611 }
612
776 // Optionally filter the results: 613 // Optionally filter the results:
777 $links = $LINKSDB->filterSearch($_GET); 614 $links = $LINKSDB->filterSearch($_GET);
778 $linksToDisplay = array(); 615 $linksToDisplay = array();
779 616
780 // Get only links which have a thumbnail. 617 // Get only links which have a thumbnail.
781 foreach($links as $link) 618 // Note: we do not retrieve thumbnails here, the request is too heavy.
619 foreach($links as $key => $link)
782 { 620 {
783 $permalink='?'.$link['shorturl']; 621 if (isset($link['thumbnail']) && $link['thumbnail'] !== false) {
784 $thumb=lazyThumbnail($conf, $link['url'],$permalink); 622 $linksToDisplay[] = $link; // Add to array.
785 if ($thumb!='') // Only output links which have a thumbnail.
786 {
787 $link['thumbnail']=$thumb; // Thumbnail HTML code.
788 $linksToDisplay[]=$link; // Add to array.
789 } 623 }
790 } 624 }
791 625
792 $data = array( 626 $data = array(
793 'linksToDisplay' => $linksToDisplay, 627 'linksToDisplay' => $linksToDisplay,
794 ); 628 );
795 $pluginManager->executeHooks('render_picwall', $data, array('loggedin' => isLoggedIn())); 629 $pluginManager->executeHooks('render_picwall', $data, array('loggedin' => $loginManager->isLoggedIn()));
796 630
797 foreach ($data as $key => $value) { 631 foreach ($data as $key => $value) {
798 $PAGE->assign($key, $value); 632 $PAGE->assign($key, $value);
799 } 633 }
800 634
635
801 $PAGE->renderPage('picwall'); 636 $PAGE->renderPage('picwall');
802 exit; 637 exit;
803 } 638 }
@@ -805,7 +640,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
805 // -------- Tag cloud 640 // -------- Tag cloud
806 if ($targetPage == Router::$PAGE_TAGCLOUD) 641 if ($targetPage == Router::$PAGE_TAGCLOUD)
807 { 642 {
808 $visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all'; 643 $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
809 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : []; 644 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
810 $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility); 645 $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility);
811 646
@@ -833,16 +668,19 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
833 ); 668 );
834 } 669 }
835 670
671 $searchTags = implode(' ', escape($filteringTags));
836 $data = array( 672 $data = array(
837 'search_tags' => implode(' ', escape($filteringTags)), 673 'search_tags' => $searchTags,
838 'tags' => $tagList, 674 'tags' => $tagList,
839 ); 675 );
840 $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn())); 676 $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => $loginManager->isLoggedIn()));
841 677
842 foreach ($data as $key => $value) { 678 foreach ($data as $key => $value) {
843 $PAGE->assign($key, $value); 679 $PAGE->assign($key, $value);
844 } 680 }
845 681
682 $searchTags = ! empty($searchTags) ? $searchTags .' - ' : '';
683 $PAGE->assign('pagetitle', $searchTags. t('Tag cloud') .' - '. $conf->get('general.title', 'Shaarli'));
846 $PAGE->renderPage('tag.cloud'); 684 $PAGE->renderPage('tag.cloud');
847 exit; 685 exit;
848 } 686 }
@@ -850,7 +688,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
850 // -------- Tag list 688 // -------- Tag list
851 if ($targetPage == Router::$PAGE_TAGLIST) 689 if ($targetPage == Router::$PAGE_TAGLIST)
852 { 690 {
853 $visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all'; 691 $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
854 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : []; 692 $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
855 $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility); 693 $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility);
856 foreach ($filteringTags as $tag) { 694 foreach ($filteringTags as $tag) {
@@ -863,23 +701,26 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
863 alphabetical_sort($tags, false, true); 701 alphabetical_sort($tags, false, true);
864 } 702 }
865 703
704 $searchTags = implode(' ', escape($filteringTags));
866 $data = [ 705 $data = [
867 'search_tags' => implode(' ', escape($filteringTags)), 706 'search_tags' => $searchTags,
868 'tags' => $tags, 707 'tags' => $tags,
869 ]; 708 ];
870 $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]); 709 $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => $loginManager->isLoggedIn()]);
871 710
872 foreach ($data as $key => $value) { 711 foreach ($data as $key => $value) {
873 $PAGE->assign($key, $value); 712 $PAGE->assign($key, $value);
874 } 713 }
875 714
715 $searchTags = ! empty($searchTags) ? $searchTags .' - ' : '';
716 $PAGE->assign('pagetitle', $searchTags . t('Tag list') .' - '. $conf->get('general.title', 'Shaarli'));
876 $PAGE->renderPage('tag.list'); 717 $PAGE->renderPage('tag.list');
877 exit; 718 exit;
878 } 719 }
879 720
880 // Daily page. 721 // Daily page.
881 if ($targetPage == Router::$PAGE_DAILY) { 722 if ($targetPage == Router::$PAGE_DAILY) {
882 showDaily($PAGE, $LINKSDB, $conf, $pluginManager); 723 showDaily($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
883 } 724 }
884 725
885 // ATOM and RSS feed. 726 // ATOM and RSS feed.
@@ -892,7 +733,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
892 $cache = new CachedPage( 733 $cache = new CachedPage(
893 $conf->get('resource.page_cache'), 734 $conf->get('resource.page_cache'),
894 page_url($_SERVER), 735 page_url($_SERVER),
895 startsWith($query,'do='. $targetPage) && !isLoggedIn() 736 startsWith($query,'do='. $targetPage) && !$loginManager->isLoggedIn()
896 ); 737 );
897 $cached = $cache->cachedVersion(); 738 $cached = $cache->cachedVersion();
898 if (!empty($cached)) { 739 if (!empty($cached)) {
@@ -901,15 +742,15 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
901 } 742 }
902 743
903 // Generate data. 744 // Generate data.
904 $feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, isLoggedIn()); 745 $feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, $loginManager->isLoggedIn());
905 $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0))); 746 $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0)));
906 $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !isLoggedIn()); 747 $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !$loginManager->isLoggedIn());
907 $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks')); 748 $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks'));
908 $data = $feedGenerator->buildData(); 749 $data = $feedGenerator->buildData();
909 750
910 // Process plugin hook. 751 // Process plugin hook.
911 $pluginManager->executeHooks('render_feed', $data, array( 752 $pluginManager->executeHooks('render_feed', $data, array(
912 'loggedin' => isLoggedIn(), 753 'loggedin' => $loginManager->isLoggedIn(),
913 'target' => $targetPage, 754 'target' => $targetPage,
914 )); 755 ));
915 756
@@ -959,7 +800,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
959 if (empty($params['searchtags'])) { 800 if (empty($params['searchtags'])) {
960 $params['searchtags'] = trim($_GET['addtag']); 801 $params['searchtags'] = trim($_GET['addtag']);
961 } 802 }
962 else if ($addtag) { 803 elseif ($addtag) {
963 $params['searchtags'] = trim($params['searchtags']).' '.trim($_GET['addtag']); 804 $params['searchtags'] = trim($params['searchtags']).' '.trim($_GET['addtag']);
964 } 805 }
965 806
@@ -1016,15 +857,26 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1016 } 857 }
1017 858
1018 // -------- User wants to see only private links (toggle) 859 // -------- User wants to see only private links (toggle)
1019 if (isset($_GET['privateonly'])) { 860 if (isset($_GET['visibility'])) {
1020 if (empty($_SESSION['privateonly'])) { 861 if ($_GET['visibility'] === 'private') {
1021 $_SESSION['privateonly'] = 1; // See only private links 862 // Visibility not set or not already private, set private, otherwise reset it
1022 } else { 863 if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'private') {
1023 unset($_SESSION['privateonly']); // See all links 864 // See only private links
865 $_SESSION['visibility'] = 'private';
866 } else {
867 unset($_SESSION['visibility']);
868 }
869 } elseif ($_GET['visibility'] === 'public') {
870 if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'public') {
871 // See only public links
872 $_SESSION['visibility'] = 'public';
873 } else {
874 unset($_SESSION['visibility']);
875 }
1024 } 876 }
1025 877
1026 if (! empty($_SERVER['HTTP_REFERER'])) { 878 if (! empty($_SERVER['HTTP_REFERER'])) {
1027 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('privateonly')); 879 $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('visibility'));
1028 } else { 880 } else {
1029 $location = '?'; 881 $location = '?';
1030 } 882 }
@@ -1046,7 +898,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1046 } 898 }
1047 899
1048 // -------- Handle other actions allowed for non-logged in users: 900 // -------- Handle other actions allowed for non-logged in users:
1049 if (!isLoggedIn()) 901 if (!$loginManager->isLoggedIn())
1050 { 902 {
1051 // User tries to post new link but is not logged in: 903 // User tries to post new link but is not logged in:
1052 // Show login screen, then redirect to ?post=... 904 // Show login screen, then redirect to ?post=...
@@ -1062,7 +914,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1062 exit; 914 exit;
1063 } 915 }
1064 916
1065 showLinkList($PAGE, $LINKSDB, $conf, $pluginManager); 917 showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
1066 if (isset($_GET['edit_link'])) { 918 if (isset($_GET['edit_link'])) {
1067 header('Location: ?do=login&edit_link='. escape($_GET['edit_link'])); 919 header('Location: ?do=login&edit_link='. escape($_GET['edit_link']));
1068 exit; 920 exit;
@@ -1086,6 +938,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1086 $PAGE->assign($key, $value); 938 $PAGE->assign($key, $value);
1087 } 939 }
1088 940
941 $PAGE->assign('pagetitle', t('Tools') .' - '. $conf->get('general.title', 'Shaarli'));
1089 $PAGE->renderPage('tools'); 942 $PAGE->renderPage('tools');
1090 exit; 943 exit;
1091 } 944 }
@@ -1112,7 +965,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1112 $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand())); 965 $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
1113 $conf->set('credentials.hash', sha1($_POST['setpassword'] . $conf->get('credentials.login') . $conf->get('credentials.salt'))); 966 $conf->set('credentials.hash', sha1($_POST['setpassword'] . $conf->get('credentials.login') . $conf->get('credentials.salt')));
1114 try { 967 try {
1115 $conf->write(isLoggedIn()); 968 $conf->write($loginManager->isLoggedIn());
1116 } 969 }
1117 catch(Exception $e) { 970 catch(Exception $e) {
1118 error_log( 971 error_log(
@@ -1129,6 +982,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1129 } 982 }
1130 else // show the change password form. 983 else // show the change password form.
1131 { 984 {
985 $PAGE->assign('pagetitle', t('Change password') .' - '. $conf->get('general.title', 'Shaarli'));
1132 $PAGE->renderPage('changepassword'); 986 $PAGE->renderPage('changepassword');
1133 exit; 987 exit;
1134 } 988 }
@@ -1152,7 +1006,6 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1152 $conf->set('general.title', escape($_POST['title'])); 1006 $conf->set('general.title', escape($_POST['title']));
1153 $conf->set('general.header_link', escape($_POST['titleLink'])); 1007 $conf->set('general.header_link', escape($_POST['titleLink']));
1154 $conf->set('resource.theme', escape($_POST['theme'])); 1008 $conf->set('resource.theme', escape($_POST['theme']));
1155 $conf->set('redirector.url', escape($_POST['redirector']));
1156 $conf->set('security.session_protection_disabled', !empty($_POST['disablesessionprotection'])); 1009 $conf->set('security.session_protection_disabled', !empty($_POST['disablesessionprotection']));
1157 $conf->set('privacy.default_private_links', !empty($_POST['privateLinkByDefault'])); 1010 $conf->set('privacy.default_private_links', !empty($_POST['privateLinkByDefault']));
1158 $conf->set('feed.rss_permalinks', !empty($_POST['enableRssPermalinks'])); 1011 $conf->set('feed.rss_permalinks', !empty($_POST['enableRssPermalinks']));
@@ -1162,8 +1015,18 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1162 $conf->set('api.secret', escape($_POST['apiSecret'])); 1015 $conf->set('api.secret', escape($_POST['apiSecret']));
1163 $conf->set('translation.language', escape($_POST['language'])); 1016 $conf->set('translation.language', escape($_POST['language']));
1164 1017
1018 $thumbnailsMode = extension_loaded('gd') ? $_POST['enableThumbnails'] : Thumbnailer::MODE_NONE;
1019 if ($thumbnailsMode !== Thumbnailer::MODE_NONE
1020 && $thumbnailsMode !== $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)
1021 ) {
1022 $_SESSION['warnings'][] = t(
1023 'You have enabled or changed thumbnails mode. <a href="?do=thumbs_update">Please synchronize them</a>.'
1024 );
1025 }
1026 $conf->set('thumbnails.mode', $thumbnailsMode);
1027
1165 try { 1028 try {
1166 $conf->write(isLoggedIn()); 1029 $conf->write($loginManager->isLoggedIn());
1167 $history->updateSettings(); 1030 $history->updateSettings();
1168 invalidateCaches($conf->get('resource.page_cache')); 1031 invalidateCaches($conf->get('resource.page_cache'));
1169 } 1032 }
@@ -1185,7 +1048,6 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1185 $PAGE->assign('title', $conf->get('general.title')); 1048 $PAGE->assign('title', $conf->get('general.title'));
1186 $PAGE->assign('theme', $conf->get('resource.theme')); 1049 $PAGE->assign('theme', $conf->get('resource.theme'));
1187 $PAGE->assign('theme_available', ThemeUtils::getThemes($conf->get('resource.raintpl_tpl'))); 1050 $PAGE->assign('theme_available', ThemeUtils::getThemes($conf->get('resource.raintpl_tpl')));
1188 $PAGE->assign('redirector', $conf->get('redirector.url'));
1189 list($continents, $cities) = generateTimeZoneData( 1051 list($continents, $cities) = generateTimeZoneData(
1190 timezone_identifiers_list(), 1052 timezone_identifiers_list(),
1191 $conf->get('general.timezone') 1053 $conf->get('general.timezone')
@@ -1201,6 +1063,9 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1201 $PAGE->assign('api_secret', $conf->get('api.secret')); 1063 $PAGE->assign('api_secret', $conf->get('api.secret'));
1202 $PAGE->assign('languages', Languages::getAvailableLanguages()); 1064 $PAGE->assign('languages', Languages::getAvailableLanguages());
1203 $PAGE->assign('language', $conf->get('translation.language')); 1065 $PAGE->assign('language', $conf->get('translation.language'));
1066 $PAGE->assign('gd_enabled', extension_loaded('gd'));
1067 $PAGE->assign('thumbnails_mode', $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE));
1068 $PAGE->assign('pagetitle', t('Configure') .' - '. $conf->get('general.title', 'Shaarli'));
1204 $PAGE->renderPage('configure'); 1069 $PAGE->renderPage('configure');
1205 exit; 1070 exit;
1206 } 1071 }
@@ -1211,6 +1076,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1211 { 1076 {
1212 if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) { 1077 if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) {
1213 $PAGE->assign('fromtag', ! empty($_GET['fromtag']) ? escape($_GET['fromtag']) : ''); 1078 $PAGE->assign('fromtag', ! empty($_GET['fromtag']) ? escape($_GET['fromtag']) : '');
1079 $PAGE->assign('pagetitle', t('Manage tags') .' - '. $conf->get('general.title', 'Shaarli'));
1214 $PAGE->renderPage('changetag'); 1080 $PAGE->renderPage('changetag');
1215 exit; 1081 exit;
1216 } 1082 }
@@ -1237,6 +1103,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1237 // -------- User wants to add a link without using the bookmarklet: Show form. 1103 // -------- User wants to add a link without using the bookmarklet: Show form.
1238 if ($targetPage == Router::$PAGE_ADDLINK) 1104 if ($targetPage == Router::$PAGE_ADDLINK)
1239 { 1105 {
1106 $PAGE->assign('pagetitle', t('Shaare a new link') .' - '. $conf->get('general.title', 'Shaarli'));
1240 $PAGE->renderPage('addlink'); 1107 $PAGE->renderPage('addlink');
1241 exit; 1108 exit;
1242 } 1109 }
@@ -1254,7 +1121,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1254 // Linkdate is kept here to: 1121 // Linkdate is kept here to:
1255 // - use the same permalink for notes as they're displayed when creating them 1122 // - use the same permalink for notes as they're displayed when creating them
1256 // - let users hack creation date of their posts 1123 // - let users hack creation date of their posts
1257 // See: https://shaarli.readthedocs.io/en/master/Various-hacks/#changing-the-timestamp-for-a-shaare 1124 // See: https://shaarli.readthedocs.io/en/master/guides/various-hacks/#changing-the-timestamp-for-a-shaare
1258 $linkdate = escape($_POST['lf_linkdate']); 1125 $linkdate = escape($_POST['lf_linkdate']);
1259 if (isset($LINKSDB[$id])) { 1126 if (isset($LINKSDB[$id])) {
1260 // Edit 1127 // Edit
@@ -1299,6 +1166,11 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1299 $link['title'] = $link['url']; 1166 $link['title'] = $link['url'];
1300 } 1167 }
1301 1168
1169 if ($conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE) {
1170 $thumbnailer = new Thumbnailer($conf);
1171 $link['thumbnail'] = $thumbnailer->get($url);
1172 }
1173
1302 $pluginManager->executeHooks('save_link', $link); 1174 $pluginManager->executeHooks('save_link', $link);
1303 1175
1304 $LINKSDB[$id] = $link; 1176 $LINKSDB[$id] = $link;
@@ -1406,6 +1278,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1406 $PAGE->assign($key, $value); 1278 $PAGE->assign($key, $value);
1407 } 1279 }
1408 1280
1281 $PAGE->assign('pagetitle', t('Edit') .' '. t('Shaare') .' - '. $conf->get('general.title', 'Shaarli'));
1409 $PAGE->renderPage('editlink'); 1282 $PAGE->renderPage('editlink');
1410 exit; 1283 exit;
1411 } 1284 }
@@ -1431,7 +1304,12 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1431 if (empty($title) && strpos(get_url_scheme($url), 'http') !== false) { 1304 if (empty($title) && strpos(get_url_scheme($url), 'http') !== false) {
1432 // Short timeout to keep the application responsive 1305 // Short timeout to keep the application responsive
1433 // The callback will fill $charset and $title with data from the downloaded page. 1306 // The callback will fill $charset and $title with data from the downloaded page.
1434 get_http_response($url, 25, 4194304, get_curl_download_callback($charset, $title)); 1307 get_http_response(
1308 $url,
1309 $conf->get('general.download_timeout', 30),
1310 $conf->get('general.download_max_size', 4194304),
1311 get_curl_download_callback($charset, $title)
1312 );
1435 if (! empty($title) && strtolower($charset) != 'utf-8') { 1313 if (! empty($title) && strtolower($charset) != 'utf-8') {
1436 $title = mb_convert_encoding($title, 'utf-8', $charset); 1314 $title = mb_convert_encoding($title, 'utf-8', $charset);
1437 } 1315 }
@@ -1470,6 +1348,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1470 $PAGE->assign($key, $value); 1348 $PAGE->assign($key, $value);
1471 } 1349 }
1472 1350
1351 $PAGE->assign('pagetitle', t('Shaare') .' - '. $conf->get('general.title', 'Shaarli'));
1473 $PAGE->renderPage('editlink'); 1352 $PAGE->renderPage('editlink');
1474 exit; 1353 exit;
1475 } 1354 }
@@ -1478,6 +1357,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1478 // Export links as a Netscape Bookmarks file 1357 // Export links as a Netscape Bookmarks file
1479 1358
1480 if (empty($_GET['selection'])) { 1359 if (empty($_GET['selection'])) {
1360 $PAGE->assign('pagetitle', t('Export') .' - '. $conf->get('general.title', 'Shaarli'));
1481 $PAGE->renderPage('export'); 1361 $PAGE->renderPage('export');
1482 exit; 1362 exit;
1483 } 1363 }
@@ -1539,6 +1419,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1539 true 1419 true
1540 ) 1420 )
1541 ); 1421 );
1422 $PAGE->assign('pagetitle', t('Import') .' - '. $conf->get('general.title', 'Shaarli'));
1542 $PAGE->renderPage('import'); 1423 $PAGE->renderPage('import');
1543 exit; 1424 exit;
1544 } 1425 }
@@ -1587,6 +1468,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1587 1468
1588 $PAGE->assign('enabledPlugins', $enabledPlugins); 1469 $PAGE->assign('enabledPlugins', $enabledPlugins);
1589 $PAGE->assign('disabledPlugins', $disabledPlugins); 1470 $PAGE->assign('disabledPlugins', $disabledPlugins);
1471 $PAGE->assign('pagetitle', t('Plugin administration') .' - '. $conf->get('general.title', 'Shaarli'));
1590 $PAGE->renderPage('pluginsadmin'); 1472 $PAGE->renderPage('pluginsadmin');
1591 exit; 1473 exit;
1592 } 1474 }
@@ -1603,7 +1485,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1603 else { 1485 else {
1604 $conf->set('general.enabled_plugins', save_plugin_config($_POST)); 1486 $conf->set('general.enabled_plugins', save_plugin_config($_POST));
1605 } 1487 }
1606 $conf->write(isLoggedIn()); 1488 $conf->write($loginManager->isLoggedIn());
1607 $history->updateSettings(); 1489 $history->updateSettings();
1608 } 1490 }
1609 catch (Exception $e) { 1491 catch (Exception $e) {
@@ -1627,8 +1509,45 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1627 exit; 1509 exit;
1628 } 1510 }
1629 1511
1512 // -------- Thumbnails Update
1513 if ($targetPage == Router::$PAGE_THUMBS_UPDATE) {
1514 $ids = [];
1515 foreach ($LINKSDB as $link) {
1516 // A note or not HTTP(S)
1517 if ($link['url'][0] === '?' || ! startsWith(strtolower($link['url']), 'http')) {
1518 continue;
1519 }
1520 $ids[] = $link['id'];
1521 }
1522 $PAGE->assign('ids', $ids);
1523 $PAGE->assign('pagetitle', t('Thumbnails update') .' - '. $conf->get('general.title', 'Shaarli'));
1524 $PAGE->renderPage('thumbnails');
1525 exit;
1526 }
1527
1528 // -------- Single Thumbnail Update
1529 if ($targetPage == Router::$AJAX_THUMB_UPDATE) {
1530 if (! isset($_POST['id']) || ! ctype_digit($_POST['id'])) {
1531 http_response_code(400);
1532 exit;
1533 }
1534 $id = (int) $_POST['id'];
1535 if (empty($LINKSDB[$id])) {
1536 http_response_code(404);
1537 exit;
1538 }
1539 $thumbnailer = new Thumbnailer($conf);
1540 $link = $LINKSDB[$id];
1541 $link['thumbnail'] = $thumbnailer->get($link['url']);
1542 $LINKSDB[$id] = $link;
1543 $LINKSDB->save($conf->get('resource.page_cache'));
1544
1545 echo json_encode($link);
1546 exit;
1547 }
1548
1630 // -------- Otherwise, simply display search form and links: 1549 // -------- Otherwise, simply display search form and links:
1631 showLinkList($PAGE, $LINKSDB, $conf, $pluginManager); 1550 showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
1632 exit; 1551 exit;
1633} 1552}
1634 1553
@@ -1640,8 +1559,9 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
1640 * @param LinkDB $LINKSDB LinkDB instance. 1559 * @param LinkDB $LINKSDB LinkDB instance.
1641 * @param ConfigManager $conf Configuration Manager instance. 1560 * @param ConfigManager $conf Configuration Manager instance.
1642 * @param PluginManager $pluginManager Plugin Manager instance. 1561 * @param PluginManager $pluginManager Plugin Manager instance.
1562 * @param LoginManager $loginManager LoginManager instance
1643 */ 1563 */
1644function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) 1564function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
1645{ 1565{
1646 // Used in templates 1566 // Used in templates
1647 if (isset($_GET['searchtags'])) { 1567 if (isset($_GET['searchtags'])) {
@@ -1666,7 +1586,7 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
1666 } 1586 }
1667 } else { 1587 } else {
1668 // Filter links according search parameters. 1588 // Filter links according search parameters.
1669 $visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all'; 1589 $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
1670 $request = [ 1590 $request = [
1671 'searchtags' => $searchtags, 1591 'searchtags' => $searchtags,
1672 'searchterm' => $searchterm, 1592 'searchterm' => $searchterm,
@@ -1680,8 +1600,6 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
1680 $keys[] = $key; 1600 $keys[] = $key;
1681 } 1601 }
1682 1602
1683
1684
1685 // Select articles according to paging. 1603 // Select articles according to paging.
1686 $pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']); 1604 $pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']);
1687 $pagecount = $pagecount == 0 ? 1 : $pagecount; 1605 $pagecount = $pagecount == 0 ? 1 : $pagecount;
@@ -1691,6 +1609,12 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
1691 // Start index. 1609 // Start index.
1692 $i = ($page-1) * $_SESSION['LINKS_PER_PAGE']; 1610 $i = ($page-1) * $_SESSION['LINKS_PER_PAGE'];
1693 $end = $i + $_SESSION['LINKS_PER_PAGE']; 1611 $end = $i + $_SESSION['LINKS_PER_PAGE'];
1612
1613 $thumbnailsEnabled = $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE;
1614 if ($thumbnailsEnabled) {
1615 $thumbnailer = new Thumbnailer($conf);
1616 }
1617
1694 $linkDisp = array(); 1618 $linkDisp = array();
1695 while ($i<$end && $i<count($keys)) 1619 while ($i<$end && $i<count($keys))
1696 { 1620 {
@@ -1711,9 +1635,21 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
1711 $taglist = preg_split('/\s+/', $link['tags'], -1, PREG_SPLIT_NO_EMPTY); 1635 $taglist = preg_split('/\s+/', $link['tags'], -1, PREG_SPLIT_NO_EMPTY);
1712 uasort($taglist, 'strcasecmp'); 1636 uasort($taglist, 'strcasecmp');
1713 $link['taglist'] = $taglist; 1637 $link['taglist'] = $taglist;
1638
1639 // Thumbnails enabled, not a note,
1640 // and (never retrieved yet or no valid cache file)
1641 if ($thumbnailsEnabled && $link['url'][0] != '?'
1642 && (! isset($link['thumbnail']) || ($link['thumbnail'] !== false && ! is_file($link['thumbnail'])))
1643 ) {
1644 $elem = $LINKSDB[$keys[$i]];
1645 $elem['thumbnail'] = $thumbnailer->get($link['url']);
1646 $LINKSDB[$keys[$i]] = $elem;
1647 $updateDB = true;
1648 $link['thumbnail'] = $elem['thumbnail'];
1649 }
1650
1714 // Check for both signs of a note: starting with ? and 7 chars long. 1651 // Check for both signs of a note: starting with ? and 7 chars long.
1715 if ($link['url'][0] === '?' && 1652 if ($link['url'][0] === '?' && strlen($link['url']) === 7) {
1716 strlen($link['url']) === 7) {
1717 $link['url'] = index_url($_SERVER) . $link['url']; 1653 $link['url'] = index_url($_SERVER) . $link['url'];
1718 } 1654 }
1719 1655
@@ -1721,6 +1657,11 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
1721 $i++; 1657 $i++;
1722 } 1658 }
1723 1659
1660 // If we retrieved new thumbnails, we update the database.
1661 if (!empty($updateDB)) {
1662 $LINKSDB->save($conf->get('resource.page_cache'));
1663 }
1664
1724 // Compute paging navigation 1665 // Compute paging navigation
1725 $searchtagsUrl = $searchtags === '' ? '' : '&searchtags=' . urlencode($searchtags); 1666 $searchtagsUrl = $searchtags === '' ? '' : '&searchtags=' . urlencode($searchtags);
1726 $searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm); 1667 $searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm);
@@ -1742,7 +1683,7 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
1742 'result_count' => count($linksToDisplay), 1683 'result_count' => count($linksToDisplay),
1743 'search_term' => $searchterm, 1684 'search_term' => $searchterm,
1744 'search_tags' => $searchtags, 1685 'search_tags' => $searchtags,
1745 'visibility' => ! empty($_SESSION['privateonly']) ? 'private' : '', 1686 'visibility' => ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '',
1746 'redirector' => $conf->get('redirector.url'), // Optional redirector URL. 1687 'redirector' => $conf->get('redirector.url'), // Optional redirector URL.
1747 'links' => $linkDisp, 1688 'links' => $linkDisp,
1748 ); 1689 );
@@ -1750,9 +1691,19 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
1750 // If there is only a single link, we change on-the-fly the title of the page. 1691 // If there is only a single link, we change on-the-fly the title of the page.
1751 if (count($linksToDisplay) == 1) { 1692 if (count($linksToDisplay) == 1) {
1752 $data['pagetitle'] = $linksToDisplay[$keys[0]]['title'] .' - '. $conf->get('general.title'); 1693 $data['pagetitle'] = $linksToDisplay[$keys[0]]['title'] .' - '. $conf->get('general.title');
1694 } elseif (! empty($searchterm) || ! empty($searchtags)) {
1695 $data['pagetitle'] = t('Search: ');
1696 $data['pagetitle'] .= ! empty($searchterm) ? $searchterm .' ' : '';
1697 $bracketWrap = function ($tag) {
1698 return '['. $tag .']';
1699 };
1700 $data['pagetitle'] .= ! empty($searchtags)
1701 ? implode(' ', array_map($bracketWrap, preg_split('/\s+/', $searchtags))).' '
1702 : '';
1703 $data['pagetitle'] .= '- '. $conf->get('general.title');
1753 } 1704 }
1754 1705
1755 $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => isLoggedIn())); 1706 $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => $loginManager->isLoggedIn()));
1756 1707
1757 foreach ($data as $key => $value) { 1708 foreach ($data as $key => $value) {
1758 $PAGE->assign($key, $value); 1709 $PAGE->assign($key, $value);
@@ -1762,201 +1713,14 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
1762} 1713}
1763 1714
1764/** 1715/**
1765 * Compute the thumbnail for a link.
1766 *
1767 * With a link to the original URL.
1768 * Understands various services (youtube.com...)
1769 * Input: $url = URL for which the thumbnail must be found.
1770 * $href = if provided, this URL will be followed instead of $url
1771 * Returns an associative array with thumbnail attributes (src,href,width,height,style,alt)
1772 * Some of them may be missing.
1773 * Return an empty array if no thumbnail available.
1774 *
1775 * @param ConfigManager $conf Configuration Manager instance.
1776 * @param string $url
1777 * @param string|bool $href
1778 *
1779 * @return array
1780 */
1781function computeThumbnail($conf, $url, $href = false)
1782{
1783 if (!$conf->get('thumbnail.enable_thumbnails')) return array();
1784 if ($href==false) $href=$url;
1785
1786 // For most hosts, the URL of the thumbnail can be easily deduced from the URL of the link.
1787 // (e.g. http://www.youtube.com/watch?v=spVypYk4kto ---> http://img.youtube.com/vi/spVypYk4kto/default.jpg )
1788 // ^^^^^^^^^^^ ^^^^^^^^^^^
1789 $domain = parse_url($url,PHP_URL_HOST);
1790 if ($domain=='youtube.com' || $domain=='www.youtube.com')
1791 {
1792 parse_str(parse_url($url,PHP_URL_QUERY), $params); // Extract video ID and get thumbnail
1793 if (!empty($params['v'])) return array('src'=>'https://img.youtube.com/vi/'.$params['v'].'/default.jpg',
1794 'href'=>$href,'width'=>'120','height'=>'90','alt'=>'YouTube thumbnail');
1795 }
1796 if ($domain=='youtu.be') // Youtube short links
1797 {
1798 $path = parse_url($url,PHP_URL_PATH);
1799 return array('src'=>'https://img.youtube.com/vi'.$path.'/default.jpg',
1800 'href'=>$href,'width'=>'120','height'=>'90','alt'=>'YouTube thumbnail');
1801 }
1802 if ($domain=='pix.toile-libre.org') // pix.toile-libre.org image hosting
1803 {
1804 parse_str(parse_url($url,PHP_URL_QUERY), $params); // Extract image filename.
1805 if (!empty($params) && !empty($params['img'])) return array('src'=>'http://pix.toile-libre.org/upload/thumb/'.urlencode($params['img']),
1806 'href'=>$href,'style'=>'max-width:120px; max-height:150px','alt'=>'pix.toile-libre.org thumbnail');
1807 }
1808
1809 if ($domain=='imgur.com')
1810 {
1811 $path = parse_url($url,PHP_URL_PATH);
1812 if (startsWith($path,'/a/')) return array(); // Thumbnails for albums are not available.
1813 if (startsWith($path,'/r/')) return array('src'=>'https://i.imgur.com/'.basename($path).'s.jpg',
1814 'href'=>$href,'width'=>'90','height'=>'90','alt'=>'imgur.com thumbnail');
1815 if (startsWith($path,'/gallery/')) return array('src'=>'https://i.imgur.com'.substr($path,8).'s.jpg',
1816 'href'=>$href,'width'=>'90','height'=>'90','alt'=>'imgur.com thumbnail');
1817
1818 if (substr_count($path,'/')==1) return array('src'=>'https://i.imgur.com/'.substr($path,1).'s.jpg',
1819 'href'=>$href,'width'=>'90','height'=>'90','alt'=>'imgur.com thumbnail');
1820 }
1821 if ($domain=='i.imgur.com')
1822 {
1823 $pi = pathinfo(parse_url($url,PHP_URL_PATH));
1824 if (!empty($pi['filename'])) return array('src'=>'https://i.imgur.com/'.$pi['filename'].'s.jpg',
1825 'href'=>$href,'width'=>'90','height'=>'90','alt'=>'imgur.com thumbnail');
1826 }
1827 if ($domain=='dailymotion.com' || $domain=='www.dailymotion.com')
1828 {
1829 if (strpos($url,'dailymotion.com/video/')!==false)
1830 {
1831 $thumburl=str_replace('dailymotion.com/video/','dailymotion.com/thumbnail/video/',$url);
1832 return array('src'=>$thumburl,
1833 'href'=>$href,'width'=>'120','style'=>'height:auto;','alt'=>'DailyMotion thumbnail');
1834 }
1835 }
1836 if (endsWith($domain,'.imageshack.us'))
1837 {
1838 $ext=strtolower(pathinfo($url,PATHINFO_EXTENSION));
1839 if ($ext=='jpg' || $ext=='jpeg' || $ext=='png' || $ext=='gif')
1840 {
1841 $thumburl = substr($url,0,strlen($url)-strlen($ext)).'th.'.$ext;
1842 return array('src'=>$thumburl,
1843 'href'=>$href,'width'=>'120','style'=>'height:auto;','alt'=>'imageshack.us thumbnail');
1844 }
1845 }
1846
1847 // Some other hosts are SLOW AS HELL and usually require an extra HTTP request to get the thumbnail URL.
1848 // So we deport the thumbnail generation in order not to slow down page generation
1849 // (and we also cache the thumbnail)
1850
1851 if (! $conf->get('thumbnail.enable_localcache')) return array(); // If local cache is disabled, no thumbnails for services which require the use a local cache.
1852
1853 if ($domain=='flickr.com' || endsWith($domain,'.flickr.com')
1854 || $domain=='vimeo.com'
1855 || $domain=='ted.com' || endsWith($domain,'.ted.com')
1856 || $domain=='xkcd.com' || endsWith($domain,'.xkcd.com')
1857 )
1858 {
1859 if ($domain=='vimeo.com')
1860 { // Make sure this vimeo URL points to a video (/xxx... where xxx is numeric)
1861 $path = parse_url($url,PHP_URL_PATH);
1862 if (!preg_match('!/\d+.+?!',$path)) return array(); // This is not a single video URL.
1863 }
1864 if ($domain=='xkcd.com' || endsWith($domain,'.xkcd.com'))
1865 { // Make sure this URL points to a single comic (/xxx... where xxx is numeric)
1866 $path = parse_url($url,PHP_URL_PATH);
1867 if (!preg_match('!/\d+.+?!',$path)) return array();
1868 }
1869 if ($domain=='ted.com' || endsWith($domain,'.ted.com'))
1870 { // Make sure this TED URL points to a video (/talks/...)
1871 $path = parse_url($url,PHP_URL_PATH);
1872 if ("/talks/" !== substr($path,0,7)) return array(); // This is not a single video URL.
1873 }
1874 $sign = hash_hmac('sha256', $url, $conf->get('credentials.salt')); // We use the salt to sign data (it's random, secret, and specific to each installation)
1875 return array('src'=>index_url($_SERVER).'?do=genthumbnail&hmac='.$sign.'&url='.urlencode($url),
1876 'href'=>$href,'width'=>'120','style'=>'height:auto;','alt'=>'thumbnail');
1877 }
1878
1879 // For all other, we try to make a thumbnail of links ending with .jpg/jpeg/png/gif
1880 // Technically speaking, we should download ALL links and check their Content-Type to see if they are images.
1881 // But using the extension will do.
1882 $ext=strtolower(pathinfo($url,PATHINFO_EXTENSION));
1883 if ($ext=='jpg' || $ext=='jpeg' || $ext=='png' || $ext=='gif')
1884 {
1885 $sign = hash_hmac('sha256', $url, $conf->get('credentials.salt')); // We use the salt to sign data (it's random, secret, and specific to each installation)
1886 return array('src'=>index_url($_SERVER).'?do=genthumbnail&hmac='.$sign.'&url='.urlencode($url),
1887 'href'=>$href,'width'=>'120','style'=>'height:auto;','alt'=>'thumbnail');
1888 }
1889 return array(); // No thumbnail.
1890
1891}
1892
1893
1894// Returns the HTML code to display a thumbnail for a link
1895// with a link to the original URL.
1896// Understands various services (youtube.com...)
1897// Input: $url = URL for which the thumbnail must be found.
1898// $href = if provided, this URL will be followed instead of $url
1899// Returns '' if no thumbnail available.
1900function thumbnail($url,$href=false)
1901{
1902 // FIXME!
1903 global $conf;
1904 $t = computeThumbnail($conf, $url,$href);
1905 if (count($t)==0) return ''; // Empty array = no thumbnail for this URL.
1906
1907 $html='<a href="'.escape($t['href']).'"><img src="'.escape($t['src']).'"';
1908 if (!empty($t['width'])) $html.=' width="'.escape($t['width']).'"';
1909 if (!empty($t['height'])) $html.=' height="'.escape($t['height']).'"';
1910 if (!empty($t['style'])) $html.=' style="'.escape($t['style']).'"';
1911 if (!empty($t['alt'])) $html.=' alt="'.escape($t['alt']).'"';
1912 $html.='></a>';
1913 return $html;
1914}
1915
1916// Returns the HTML code to display a thumbnail for a link
1917// for the picture wall (using lazy image loading)
1918// Understands various services (youtube.com...)
1919// Input: $url = URL for which the thumbnail must be found.
1920// $href = if provided, this URL will be followed instead of $url
1921// Returns '' if no thumbnail available.
1922function lazyThumbnail($conf, $url,$href=false)
1923{
1924 // FIXME!
1925 global $conf;
1926 $t = computeThumbnail($conf, $url,$href);
1927 if (count($t)==0) return ''; // Empty array = no thumbnail for this URL.
1928
1929 $html='<a href="'.escape($t['href']).'">';
1930
1931 // Lazy image
1932 $html.='<img class="b-lazy" src="#" data-src="'.escape($t['src']).'"';
1933
1934 if (!empty($t['width'])) $html.=' width="'.escape($t['width']).'"';
1935 if (!empty($t['height'])) $html.=' height="'.escape($t['height']).'"';
1936 if (!empty($t['style'])) $html.=' style="'.escape($t['style']).'"';
1937 if (!empty($t['alt'])) $html.=' alt="'.escape($t['alt']).'"';
1938 $html.='>';
1939
1940 // No-JavaScript fallback.
1941 $html.='<noscript><img src="'.escape($t['src']).'"';
1942 if (!empty($t['width'])) $html.=' width="'.escape($t['width']).'"';
1943 if (!empty($t['height'])) $html.=' height="'.escape($t['height']).'"';
1944 if (!empty($t['style'])) $html.=' style="'.escape($t['style']).'"';
1945 if (!empty($t['alt'])) $html.=' alt="'.escape($t['alt']).'"';
1946 $html.='></noscript></a>';
1947
1948 return $html;
1949}
1950
1951
1952/**
1953 * Installation 1716 * Installation
1954 * This function should NEVER be called if the file data/config.php exists. 1717 * This function should NEVER be called if the file data/config.php exists.
1955 * 1718 *
1956 * @param ConfigManager $conf Configuration Manager instance. 1719 * @param ConfigManager $conf Configuration Manager instance.
1957 * @param SessionManager $sessionManager SessionManager instance 1720 * @param SessionManager $sessionManager SessionManager instance
1721 * @param LoginManager $loginManager LoginManager instance
1958 */ 1722 */
1959function install($conf, $sessionManager) { 1723function install($conf, $sessionManager, $loginManager) {
1960 // On free.fr host, make sure the /sessions directory exists, otherwise login will not work. 1724 // On free.fr host, make sure the /sessions directory exists, otherwise login will not work.
1961 if (endsWith($_SERVER['HTTP_HOST'],'.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions',0705); 1725 if (endsWith($_SERVER['HTTP_HOST'],'.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions',0705);
1962 1726
@@ -2023,7 +1787,7 @@ function install($conf, $sessionManager) {
2023 ); 1787 );
2024 try { 1788 try {
2025 // Everything is ok, let's create config file. 1789 // Everything is ok, let's create config file.
2026 $conf->write(isLoggedIn()); 1790 $conf->write($loginManager->isLoggedIn());
2027 } 1791 }
2028 catch(Exception $e) { 1792 catch(Exception $e) {
2029 error_log( 1793 error_log(
@@ -2039,7 +1803,7 @@ function install($conf, $sessionManager) {
2039 exit; 1803 exit;
2040 } 1804 }
2041 1805
2042 $PAGE = new PageBuilder($conf, null, $sessionManager->generateToken()); 1806 $PAGE = new PageBuilder($conf, $_SESSION, null, $sessionManager->generateToken());
2043 list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get()); 1807 list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
2044 $PAGE->assign('continents', $continents); 1808 $PAGE->assign('continents', $continents);
2045 $PAGE->assign('cities', $cities); 1809 $PAGE->assign('cities', $cities);
@@ -2048,232 +1812,6 @@ function install($conf, $sessionManager) {
2048 exit; 1812 exit;
2049} 1813}
2050 1814
2051/**
2052 * Because some f*cking services like flickr require an extra HTTP request to get the thumbnail URL,
2053 * I have deported the thumbnail URL code generation here, otherwise this would slow down page generation.
2054 * The following function takes the URL a link (e.g. a flickr page) and return the proper thumbnail.
2055 * This function is called by passing the URL:
2056 * http://mywebsite.com/shaarli/?do=genthumbnail&hmac=[HMAC]&url=[URL]
2057 * [URL] is the URL of the link (e.g. a flickr page)
2058 * [HMAC] is the signature for the [URL] (so that these URL cannot be forged).
2059 * The function below will fetch the image from the webservice and store it in the cache.
2060 *
2061 * @param ConfigManager $conf Configuration Manager instance,
2062 */
2063function genThumbnail($conf)
2064{
2065 // Make sure the parameters in the URL were generated by us.
2066 $sign = hash_hmac('sha256', $_GET['url'], $conf->get('credentials.salt'));
2067 if ($sign!=$_GET['hmac']) die('Naughty boy!');
2068
2069 $cacheDir = $conf->get('resource.thumbnails_cache', 'cache');
2070 // Let's see if we don't already have the image for this URL in the cache.
2071 $thumbname=hash('sha1',$_GET['url']).'.jpg';
2072 if (is_file($cacheDir .'/'. $thumbname))
2073 { // We have the thumbnail, just serve it:
2074 header('Content-Type: image/jpeg');
2075 echo file_get_contents($cacheDir .'/'. $thumbname);
2076 return;
2077 }
2078 // We may also serve a blank image (if service did not respond)
2079 $blankname=hash('sha1',$_GET['url']).'.gif';
2080 if (is_file($cacheDir .'/'. $blankname))
2081 {
2082 header('Content-Type: image/gif');
2083 echo file_get_contents($cacheDir .'/'. $blankname);
2084 return;
2085 }
2086
2087 // Otherwise, generate the thumbnail.
2088 $url = $_GET['url'];
2089 $domain = parse_url($url,PHP_URL_HOST);
2090
2091 if ($domain=='flickr.com' || endsWith($domain,'.flickr.com'))
2092 {
2093 // Crude replacement to handle new flickr domain policy (They prefer www. now)
2094 $url = str_replace('http://flickr.com/','http://www.flickr.com/',$url);
2095
2096 // Is this a link to an image, or to a flickr page ?
2097 $imageurl='';
2098 if (endsWith(parse_url($url, PHP_URL_PATH), '.jpg'))
2099 { // This is a direct link to an image. e.g. http://farm1.staticflickr.com/5/5921913_ac83ed27bd_o.jpg
2100 preg_match('!(http://farm\d+\.staticflickr\.com/\d+/\d+_\w+_)\w.jpg!',$url,$matches);
2101 if (!empty($matches[1])) $imageurl=$matches[1].'m.jpg';
2102 }
2103 else // This is a flickr page (html)
2104 {
2105 // Get the flickr html page.
2106 list($headers, $content) = get_http_response($url, 20);
2107 if (strpos($headers[0], '200 OK') !== false)
2108 {
2109 // flickr now nicely provides the URL of the thumbnail in each flickr page.
2110 preg_match('!<link rel=\"image_src\" href=\"(.+?)\"!', $content, $matches);
2111 if (!empty($matches[1])) $imageurl=$matches[1];
2112
2113 // In albums (and some other pages), the link rel="image_src" is not provided,
2114 // but flickr provides:
2115 // <meta property="og:image" content="http://farm4.staticflickr.com/3398/3239339068_25d13535ff_z.jpg" />
2116 if ($imageurl=='')
2117 {
2118 preg_match('!<meta property=\"og:image\" content=\"(.+?)\"!', $content, $matches);
2119 if (!empty($matches[1])) $imageurl=$matches[1];
2120 }
2121 }
2122 }
2123
2124 if ($imageurl!='')
2125 { // Let's download the image.
2126 // Image is 240x120, so 10 seconds to download should be enough.
2127 list($headers, $content) = get_http_response($imageurl, 10);
2128 if (strpos($headers[0], '200 OK') !== false) {
2129 // Save image to cache.
2130 file_put_contents($cacheDir .'/'. $thumbname, $content);
2131 header('Content-Type: image/jpeg');
2132 echo $content;
2133 return;
2134 }
2135 }
2136 }
2137
2138 elseif ($domain=='vimeo.com' )
2139 {
2140 // This is more complex: we have to perform a HTTP request, then parse the result.
2141 // Maybe we should deport this to JavaScript ? Example: http://stackoverflow.com/questions/1361149/get-img-thumbnails-from-vimeo/4285098#4285098
2142 $vid = substr(parse_url($url,PHP_URL_PATH),1);
2143 list($headers, $content) = get_http_response('https://vimeo.com/api/v2/video/'.escape($vid).'.php', 5);
2144 if (strpos($headers[0], '200 OK') !== false) {
2145 $t = unserialize($content);
2146 $imageurl = $t[0]['thumbnail_medium'];
2147 // Then we download the image and serve it to our client.
2148 list($headers, $content) = get_http_response($imageurl, 10);
2149 if (strpos($headers[0], '200 OK') !== false) {
2150 // Save image to cache.
2151 file_put_contents($cacheDir .'/'. $thumbname, $content);
2152 header('Content-Type: image/jpeg');
2153 echo $content;
2154 return;
2155 }
2156 }
2157 }
2158
2159 elseif ($domain=='ted.com' || endsWith($domain,'.ted.com'))
2160 {
2161 // The thumbnail for TED talks is located in the <link rel="image_src" [...]> tag on that page
2162 // http://www.ted.com/talks/mikko_hypponen_fighting_viruses_defending_the_net.html
2163 // <link rel="image_src" href="http://images.ted.com/images/ted/28bced335898ba54d4441809c5b1112ffaf36781_389x292.jpg" />
2164 list($headers, $content) = get_http_response($url, 5);
2165 if (strpos($headers[0], '200 OK') !== false) {
2166 // Extract the link to the thumbnail
2167 preg_match('!link rel="image_src" href="(http://images.ted.com/images/ted/.+_\d+x\d+\.jpg)"!', $content, $matches);
2168 if (!empty($matches[1]))
2169 { // Let's download the image.
2170 $imageurl=$matches[1];
2171 // No control on image size, so wait long enough
2172 list($headers, $content) = get_http_response($imageurl, 20);
2173 if (strpos($headers[0], '200 OK') !== false) {
2174 $filepath = $cacheDir .'/'. $thumbname;
2175 file_put_contents($filepath, $content); // Save image to cache.
2176 if (resizeImage($filepath))
2177 {
2178 header('Content-Type: image/jpeg');
2179 echo file_get_contents($filepath);
2180 return;
2181 }
2182 }
2183 }
2184 }
2185 }
2186
2187 elseif ($domain=='xkcd.com' || endsWith($domain,'.xkcd.com'))
2188 {
2189 // There is no thumbnail available for xkcd comics, so download the whole image and resize it.
2190 // http://xkcd.com/327/
2191 // <img src="http://imgs.xkcd.com/comics/exploits_of_a_mom.png" title="<BLABLA>" alt="<BLABLA>" />
2192 list($headers, $content) = get_http_response($url, 5);
2193 if (strpos($headers[0], '200 OK') !== false) {
2194 // Extract the link to the thumbnail
2195 preg_match('!<img src="(http://imgs.xkcd.com/comics/.*)" title="[^s]!', $content, $matches);
2196 if (!empty($matches[1]))
2197 { // Let's download the image.
2198 $imageurl=$matches[1];
2199 // No control on image size, so wait long enough
2200 list($headers, $content) = get_http_response($imageurl, 20);
2201 if (strpos($headers[0], '200 OK') !== false) {
2202 $filepath = $cacheDir.'/'.$thumbname;
2203 // Save image to cache.
2204 file_put_contents($filepath, $content);
2205 if (resizeImage($filepath))
2206 {
2207 header('Content-Type: image/jpeg');
2208 echo file_get_contents($filepath);
2209 return;
2210 }
2211 }
2212 }
2213 }
2214 }
2215
2216 else
2217 {
2218 // For all other domains, we try to download the image and make a thumbnail.
2219 // We allow 30 seconds max to download (and downloads are limited to 4 Mb)
2220 list($headers, $content) = get_http_response($url, 30);
2221 if (strpos($headers[0], '200 OK') !== false) {
2222 $filepath = $cacheDir .'/'.$thumbname;
2223 // Save image to cache.
2224 file_put_contents($filepath, $content);
2225 if (resizeImage($filepath))
2226 {
2227 header('Content-Type: image/jpeg');
2228 echo file_get_contents($filepath);
2229 return;
2230 }
2231 }
2232 }
2233
2234
2235 // Otherwise, return an empty image (8x8 transparent gif)
2236 $blankgif = base64_decode('R0lGODlhCAAIAIAAAP///////yH5BAEKAAEALAAAAAAIAAgAAAIHjI+py+1dAAA7');
2237 // Also put something in cache so that this URL is not requested twice.
2238 file_put_contents($cacheDir .'/'. $blankname, $blankgif);
2239 header('Content-Type: image/gif');
2240 echo $blankgif;
2241}
2242
2243// Make a thumbnail of the image (to width: 120 pixels)
2244// Returns true if success, false otherwise.
2245function resizeImage($filepath)
2246{
2247 if (!function_exists('imagecreatefromjpeg')) return false; // GD not present: no thumbnail possible.
2248
2249 // Trick: some stupid people rename GIF as JPEG... or else.
2250 // So we really try to open each image type whatever the extension is.
2251 $header=file_get_contents($filepath,false,NULL,0,256); // Read first 256 bytes and try to sniff file type.
2252 $im=false;
2253 $i=strpos($header,'GIF8'); if (($i!==false) && ($i==0)) $im = imagecreatefromgif($filepath); // Well this is crude, but it should be enough.
2254 $i=strpos($header,'PNG'); if (($i!==false) && ($i==1)) $im = imagecreatefrompng($filepath);
2255 $i=strpos($header,'JFIF'); if ($i!==false) $im = imagecreatefromjpeg($filepath);
2256 if (!$im) return false; // Unable to open image (corrupted or not an image)
2257 $w = imagesx($im);
2258 $h = imagesy($im);
2259 $ystart = 0; $yheight=$h;
2260 if ($h>$w) { $ystart= ($h/2)-($w/2); $yheight=$w/2; }
2261 $nw = 120; // Desired width
2262 $nh = min(floor(($h*$nw)/$w),120); // Compute new width/height, but maximum 120 pixels height.
2263 // Resize image:
2264 $im2 = imagecreatetruecolor($nw,$nh);
2265 imagecopyresampled($im2, $im, 0, 0, 0, $ystart, $nw, $nh, $w, $yheight);
2266 imageinterlace($im2,true); // For progressive JPEG.
2267 $tempname=$filepath.'_TEMP.jpg';
2268 imagejpeg($im2, $tempname, 90);
2269 imagedestroy($im);
2270 imagedestroy($im2);
2271 unlink($filepath);
2272 rename($tempname,$filepath); // Overwrite original picture with thumbnail.
2273 return true;
2274}
2275
2276if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=genthumbnail')) { genThumbnail($conf); exit; } // Thumbnail generation/cache does not need the link database.
2277if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=dailyrss')) { showDailyRSS($conf); exit; } 1815if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=dailyrss')) { showDailyRSS($conf); exit; }
2278if (!isset($_SESSION['LINKS_PER_PAGE'])) { 1816if (!isset($_SESSION['LINKS_PER_PAGE'])) {
2279 $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20); 1817 $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20);
@@ -2287,7 +1825,7 @@ try {
2287 1825
2288$linkDb = new LinkDB( 1826$linkDb = new LinkDB(
2289 $conf->get('resource.datastore'), 1827 $conf->get('resource.datastore'),
2290 isLoggedIn(), 1828 $loginManager->isLoggedIn(),
2291 $conf->get('privacy.hide_public_links'), 1829 $conf->get('privacy.hide_public_links'),
2292 $conf->get('redirector.url'), 1830 $conf->get('redirector.url'),
2293 $conf->get('redirector.encode_url') 1831 $conf->get('redirector.encode_url')
@@ -2307,6 +1845,12 @@ $app->group('/api/v1', function() {
2307 $this->post('/links', '\Shaarli\Api\Controllers\Links:postLink')->setName('postLink'); 1845 $this->post('/links', '\Shaarli\Api\Controllers\Links:postLink')->setName('postLink');
2308 $this->put('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:putLink')->setName('putLink'); 1846 $this->put('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:putLink')->setName('putLink');
2309 $this->delete('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:deleteLink')->setName('deleteLink'); 1847 $this->delete('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:deleteLink')->setName('deleteLink');
1848
1849 $this->get('/tags', '\Shaarli\Api\Controllers\Tags:getTags')->setName('getTags');
1850 $this->get('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:getTag')->setName('getTag');
1851 $this->put('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:putTag')->setName('putTag');
1852 $this->delete('/tags/{tagName:[\w]+}', '\Shaarli\Api\Controllers\Tags:deleteTag')->setName('deleteTag');
1853
2310 $this->get('/history', '\Shaarli\Api\Controllers\History:getHistory')->setName('getHistory'); 1854 $this->get('/history', '\Shaarli\Api\Controllers\History:getHistory')->setName('getHistory');
2311})->add('\Shaarli\Api\ApiMiddleware'); 1855})->add('\Shaarli\Api\ApiMiddleware');
2312 1856
@@ -2316,7 +1860,7 @@ $response = $app->run(true);
2316if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) { 1860if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) {
2317 // We use UTF-8 for proper international characters handling. 1861 // We use UTF-8 for proper international characters handling.
2318 header('Content-Type: text/html; charset=utf-8'); 1862 header('Content-Type: text/html; charset=utf-8');
2319 renderPage($conf, $pluginManager, $linkDb, $history, $sessionManager); 1863 renderPage($conf, $pluginManager, $linkDb, $history, $sessionManager, $loginManager);
2320} else { 1864} else {
2321 $app->respond($response); 1865 $app->respond($response);
2322} 1866}