X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=index.php;h=4068a828f10293ee40a8e245e5052e908cbe8bf3;hb=be9ddff2fb8706ce575e95e8cd64458411895dbe;hp=6eaa56c210931d5e5afcb296e71e51fc1614ae66;hpb=0843848c1d18e92504c43d181063a2012f8fd5b9;p=github%2Fshaarli%2FShaarli.git diff --git a/index.php b/index.php index 6eaa56c2..4068a828 100644 --- a/index.php +++ b/index.php @@ -48,8 +48,8 @@ if (! file_exists(__DIR__ . '/vendor/autoload.php')) { ."If you installed Shaarli through Git or using the development branch,\n" ."please refer to the installation documentation to install PHP" ." dependencies using Composer:\n" - ."- https://github.com/shaarli/Shaarli/wiki/Server-requirements\n" - ."- https://github.com/shaarli/Shaarli/wiki/Download-and-Installation"; + ."- https://shaarli.readthedocs.io/en/master/Server-requirements/\n" + ."- https://shaarli.readthedocs.io/en/master/Download-and-Installation/"; exit; } require_once 'inc/rain.tpl.class.php'; @@ -62,6 +62,7 @@ require_once 'application/CachedPage.php'; require_once 'application/config/ConfigPlugin.php'; require_once 'application/FeedBuilder.php'; require_once 'application/FileUtils.php'; +require_once 'application/History.php'; require_once 'application/HttpUtils.php'; require_once 'application/Languages.php'; require_once 'application/LinkDB.php'; @@ -87,7 +88,7 @@ try { exit; } -define('shaarli_version', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE)); +define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE)); // Force cookie path (but do not change lifetime) $cookie = session_get_cookie_params(); @@ -132,15 +133,6 @@ date_default_timezone_set($conf->get('general.timezone', 'UTC')); ob_start(); // Output buffering for the page cache. -// In case stupid admin has left magic_quotes enabled in php.ini: -if (get_magic_quotes_gpc()) -{ - function stripslashes_deep($value) { $value = is_array($value) ? array_map('stripslashes_deep', $value) : stripslashes($value); return $value; } - $_POST = array_map('stripslashes_deep', $_POST); - $_GET = array_map('stripslashes_deep', $_GET); - $_COOKIE = array_map('stripslashes_deep', $_COOKIE); -} - // Prevent caching on client side or proxy: (yes, it's ugly) header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); header("Cache-Control: no-store, no-cache, must-revalidate"); @@ -185,65 +177,44 @@ if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { */ function setup_login_state($conf) { - if ($conf->get('security.open_shaarli')) { - return true; - } - $userIsLoggedIn = false; // By default, we do not consider the user as logged in; - $loginFailure = false; // If set to true, every attempt to authenticate the user will fail. This indicates that an important condition isn't met. - if (! $conf->exists('credentials.login')) { - $userIsLoggedIn = false; // Shaarli is not configured yet. - $loginFailure = true; - } - if (isset($_COOKIE['shaarli_staySignedIn']) && - $_COOKIE['shaarli_staySignedIn']===STAY_SIGNED_IN_TOKEN && - !$loginFailure) - { - fillSessionInfo($conf); - $userIsLoggedIn = true; - } - // If session does not exist on server side, or IP address has changed, or session has expired, logout. - if (empty($_SESSION['uid']) + if ($conf->get('security.open_shaarli')) { + return true; + } + $userIsLoggedIn = false; // By default, we do not consider the user as logged in; + $loginFailure = false; // If set to true, every attempt to authenticate the user will fail. This indicates that an important condition isn't met. + if (! $conf->exists('credentials.login')) { + $userIsLoggedIn = false; // Shaarli is not configured yet. + $loginFailure = true; + } + if (isset($_COOKIE['shaarli_staySignedIn']) && + $_COOKIE['shaarli_staySignedIn']===STAY_SIGNED_IN_TOKEN && + !$loginFailure) + { + fillSessionInfo($conf); + $userIsLoggedIn = true; + } + // If session does not exist on server side, or IP address has changed, or session has expired, logout. + if (empty($_SESSION['uid']) || ($conf->get('security.session_protection_disabled') === false && $_SESSION['ip'] != allIPs()) || time() >= $_SESSION['expires_on']) - { - logout(); - $userIsLoggedIn = false; - $loginFailure = true; - } - if (!empty($_SESSION['longlastingsession'])) { - $_SESSION['expires_on']=time()+$_SESSION['longlastingsession']; // In case of "Stay signed in" checked. - } - else { - $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Standard session expiration date. - } - if (!$loginFailure) { - $userIsLoggedIn = true; - } - - return $userIsLoggedIn; -} -$userIsLoggedIn = setup_login_state($conf); - -/** - * PubSubHubbub protocol support (if enabled) [UNTESTED] - * (Source: http://aldarone.fr/les-flux-rss-shaarli-et-pubsubhubbub/ ) - * - * @param ConfigManager $conf Configuration Manager instance. - */ -function pubsubhub($conf) -{ - $pshUrl = $conf->get('config.PUBSUBHUB_URL'); - if (!empty($pshUrl)) { - include_once './publisher.php'; - $p = new Publisher($pshUrl); - $topic_url = array ( - index_url($_SERVER).'?do=atom', - index_url($_SERVER).'?do=rss' - ); - $p->publish_update($topic_url); + logout(); + $userIsLoggedIn = false; + $loginFailure = true; + } + if (!empty($_SESSION['longlastingsession'])) { + $_SESSION['expires_on']=time()+$_SESSION['longlastingsession']; // In case of "Stay signed in" checked. } + else { + $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Standard session expiration date. + } + if (!$loginFailure) { + $userIsLoggedIn = true; + } + + return $userIsLoggedIn; } +$userIsLoggedIn = setup_login_state($conf); // ------------------------------------------------------------------------------------------ // Session management @@ -265,10 +236,10 @@ function allIPs() */ function fillSessionInfo($conf) { - $_SESSION['uid'] = sha1(uniqid('',true).'_'.mt_rand()); // Generate unique random number (different than phpsessionid) - $_SESSION['ip']=allIPs(); // We store IP address(es) of the client to make sure session is not hijacked. - $_SESSION['username']= $conf->get('credentials.login'); - $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Set session expiration. + $_SESSION['uid'] = sha1(uniqid('',true).'_'.mt_rand()); // Generate unique random number (different than phpsessionid) + $_SESSION['ip']=allIPs(); // We store IP address(es) of the client to make sure session is not hijacked. + $_SESSION['username']= $conf->get('credentials.login'); + $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Set session expiration. } /** @@ -285,7 +256,7 @@ function check_auth($login, $password, $conf) $hash = sha1($password . $login . $conf->get('credentials.salt')); if ($login == $conf->get('credentials.login') && $hash == $conf->get('credentials.hash')) { // Login/password is correct. - fillSessionInfo($conf); + fillSessionInfo($conf); logm($conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], 'Login successful'); return true; } @@ -307,6 +278,7 @@ function logout() { unset($_SESSION['ip']); unset($_SESSION['username']); unset($_SESSION['privateonly']); + unset($_SESSION['untaggedonly']); } setcookie('shaarli_staySignedIn', FALSE, 0, WEB_PATH); } @@ -413,9 +385,10 @@ if (isset($_POST['login'])) // If user wants to keep the session cookie even after the browser closes: if (!empty($_POST['longlastingsession'])) { - setcookie('shaarli_staySignedIn', STAY_SIGNED_IN_TOKEN, time()+31536000, WEB_PATH); - $_SESSION['longlastingsession']=31536000; // (31536000 seconds = 1 year) - $_SESSION['expires_on']=time()+$_SESSION['longlastingsession']; // Set session expiration on server-side. + $_SESSION['longlastingsession'] = 31536000; // (31536000 seconds = 1 year) + $expiration = time() + $_SESSION['longlastingsession']; // calculate relative cookie expiration (1 year from now) + setcookie('shaarli_staySignedIn', STAY_SIGNED_IN_TOKEN, $expiration, WEB_PATH); + $_SESSION['expires_on'] = $expiration; // Set session expiration on server-side. $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/'; session_set_cookie_params($_SESSION['longlastingsession'],$cookiedir,$_SERVER['SERVER_NAME']); // Set session cookie expiration on client side @@ -472,34 +445,6 @@ if (isset($_POST['login'])) } } -// ------------------------------------------------------------------------------------------ -// Misc utility functions: - -// Convert post_max_size/upload_max_filesize (e.g. '16M') parameters to bytes. -function return_bytes($val) -{ - $val = trim($val); $last=strtolower($val[strlen($val)-1]); - switch($last) - { - case 'g': $val *= 1024; - case 'm': $val *= 1024; - case 'k': $val *= 1024; - } - return $val; -} - -// Try to determine max file size for uploads (POST). -// Returns an integer (in bytes) -function getMaxFileSize() -{ - $size1 = return_bytes(ini_get('post_max_size')); - $size2 = return_bytes(ini_get('upload_max_filesize')); - // Return the smaller of two: - $maxsize = min($size1,$size2); - // FIXME: Then convert back to readable notations ? (e.g. 2M instead of 2000000) - return $maxsize; -} - // ------------------------------------------------------------------------------------------ // Token management for XSRF protection // Token should be used in any form which acts on data (create,update,delete,import...). @@ -638,20 +583,29 @@ function showDailyRSS($conf) { */ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) { - $day=date('Ymd',strtotime('-1 day')); // Yesterday, in format YYYYMMDD. - if (isset($_GET['day'])) $day=$_GET['day']; + $day = date('Ymd', strtotime('-1 day')); // Yesterday, in format YYYYMMDD. + if (isset($_GET['day'])) { + $day = $_GET['day']; + } $days = $LINKSDB->days(); - $i = array_search($day,$days); - if ($i===false) { $i=count($days)-1; $day=$days[$i]; } - $previousday=''; - $nextday=''; - if ($i!==false) - { - if ($i>=1) $previousday=$days[$i-1]; - if ($i= 1) { + $previousday=$days[$i - 1]; + } + if ($i < count($days) - 1) { + $nextday = $days[$i + 1]; + } + } try { $linksToDisplay = $LINKSDB->filterDay($day); } catch (Exception $exc) { @@ -660,9 +614,7 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) } // We pre-format some fields for proper output. - foreach($linksToDisplay as $key=>$link) - { - + foreach($linksToDisplay as $key => $link) { $taglist = explode(' ',$link['tags']); uasort($taglist, 'strcasecmp'); $linksToDisplay[$key]['taglist']=$taglist; @@ -676,21 +628,22 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) so I manually spread entries with a simple method: I roughly evaluate the height of a div according to title and description length. */ - $columns=array(array(),array(),array()); // Entries to display, for each column. - $fill=array(0,0,0); // Rough estimate of columns fill. - foreach($linksToDisplay as $key=>$link) - { + $columns = array(array(), array(), array()); // Entries to display, for each column. + $fill = array(0, 0, 0); // Rough estimate of columns fill. + foreach($linksToDisplay as $key => $link) { // Roughly estimate length of entry (by counting characters) // Title: 30 chars = 1 line. 1 line is 30 pixels height. // Description: 836 characters gives roughly 342 pixel height. // This is not perfect, but it's usually OK. - $length=strlen($link['title'])+(342*strlen($link['description']))/836; - if ($link['thumbnail']) $length +=100; // 1 thumbnails roughly takes 100 pixels height. + $length = strlen($link['title']) + (342 * strlen($link['description'])) / 836; + if ($link['thumbnail']) { + $length += 100; // 1 thumbnails roughly takes 100 pixels height. + } // Then put in column which is the less filled: - $smallest=min($fill); // find smallest value in array. - $index=array_search($smallest,$fill); // find index of this smallest value. - array_push($columns[$index],$link); // Put entry in this column. - $fill[$index]+=$length; + $smallest = min($fill); // find smallest value in array. + $index = array_search($smallest, $fill); // find index of this smallest value. + array_push($columns[$index], $link); // Put entry in this column. + $fill[$index] += $length; } $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000'); @@ -733,8 +686,9 @@ function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager) { * @param ConfigManager $conf Configuration Manager instance. * @param PluginManager $pluginManager Plugin Manager instance, * @param LinkDB $LINKSDB + * @param History $history instance */ -function renderPage($conf, $pluginManager, $LINKSDB) +function renderPage($conf, $pluginManager, $LINKSDB, $history) { $updater = new Updater( read_updates_file($conf->get('resource.updates')), @@ -755,7 +709,7 @@ function renderPage($conf, $pluginManager, $LINKSDB) die($e->getMessage()); } - $PAGE = new PageBuilder($conf); + $PAGE = new PageBuilder($conf, $LINKSDB); $PAGE->assign('linkcount', count($LINKSDB)); $PAGE->assign('privateLinkcount', count_private($LINKSDB)); $PAGE->assign('plugin_errors', $pluginManager->getErrors()); @@ -764,6 +718,23 @@ function renderPage($conf, $pluginManager, $LINKSDB) $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : ''; $targetPage = Router::findPage($query, $_GET, isLoggedIn()); + if ( + // if the user isn't logged in + !isLoggedIn() && + // and Shaarli doesn't have public content... + $conf->get('privacy.hide_public_links') && + // and is configured to enforce the login + $conf->get('privacy.force_login') && + // and the current page isn't already the login page + $targetPage !== Router::$PAGE_LOGIN && + // and the user is not requesting a feed (which would lead to a different content-type as expected) + $targetPage !== Router::$PAGE_FEED_ATOM && + $targetPage !== Router::$PAGE_FEED_RSS + ) { + // force current page to be the login page + $targetPage = Router::$PAGE_LOGIN; + } + // Call plugin hooks for header, footer and includes, specifying which page will be rendered. // Then assign generated data to RainTPL. $common_hooks = array( @@ -791,6 +762,8 @@ function renderPage($conf, $pluginManager, $LINKSDB) $PAGE->assign('username', escape($_GET['username'])); } $PAGE->assign('returnurl',(isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']):'')); + // add default state of the 'remember me' checkbox + $PAGE->assign('remember_user_default', $conf->get('privacy.remember_user_default')); $PAGE->renderPage('loginform'); exit; } @@ -838,7 +811,9 @@ function renderPage($conf, $pluginManager, $LINKSDB) // -------- Tag cloud if ($targetPage == Router::$PAGE_TAGCLOUD) { - $tags= $LINKSDB->allTags(); + $visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all'; + $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : []; + $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility); // We sort tags alphabetically, then choose a font size according to count. // First, find max value. @@ -847,20 +822,13 @@ function renderPage($conf, $pluginManager, $LINKSDB) $maxcount = max($maxcount, $value); } - // Sort tags alphabetically: case insensitive, support locale if available. - uksort($tags, function($a, $b) { - // Collator is part of PHP intl. - if (class_exists('Collator')) { - $c = new Collator(setlocale(LC_COLLATE, 0)); - if (!intl_is_failure(intl_get_error_code())) { - return $c->compare($a, $b); - } - } - return strcasecmp($a, $b); - }); + alphabetical_sort($tags, false, true); $tagList = array(); foreach($tags as $key => $value) { + if (in_array($key, $filteringTags)) { + continue; + } // Tag font size scaling: // default 15 and 30 logarithm bases affect scaling, // 22 and 6 are arbitrary font sizes for max and min sizes. @@ -872,6 +840,7 @@ function renderPage($conf, $pluginManager, $LINKSDB) } $data = array( + 'search_tags' => implode(' ', escape($filteringTags)), 'tags' => $tagList, ); $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn())); @@ -880,7 +849,37 @@ function renderPage($conf, $pluginManager, $LINKSDB) $PAGE->assign($key, $value); } - $PAGE->renderPage('tagcloud'); + $PAGE->renderPage('tag.cloud'); + exit; + } + + // -------- Tag list + if ($targetPage == Router::$PAGE_TAGLIST) + { + $visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all'; + $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : []; + $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility); + foreach ($filteringTags as $tag) { + if (array_key_exists($tag, $tags)) { + unset($tags[$tag]); + } + } + + if (! empty($_GET['sort']) && $_GET['sort'] === 'alpha') { + alphabetical_sort($tags, false, true); + } + + $data = [ + 'search_tags' => implode(' ', escape($filteringTags)), + 'tags' => $tags, + ]; + $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]); + + foreach ($data as $key => $value) { + $PAGE->assign($key, $value); + } + + $PAGE->renderPage('tag.list'); exit; } @@ -1039,6 +1038,19 @@ function renderPage($conf, $pluginManager, $LINKSDB) exit; } + // -------- User wants to see only untagged links (toggle) + if (isset($_GET['untaggedonly'])) { + $_SESSION['untaggedonly'] = empty($_SESSION['untaggedonly']); + + if (! empty($_SERVER['HTTP_REFERER'])) { + $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('untaggedonly')); + } else { + $location = '?'; + } + header('Location: '. $location); + exit; + } + // -------- Handle other actions allowed for non-logged in users: if (!isLoggedIn()) { @@ -1070,10 +1082,10 @@ function renderPage($conf, $pluginManager, $LINKSDB) // -------- Display the Tools menu if requested (import/export/bookmarklet...) if ($targetPage == Router::$PAGE_TOOLS) { - $data = array( + $data = [ 'pageabsaddr' => index_url($_SERVER), - 'sslenabled' => !empty($_SERVER['HTTPS']) - ); + 'sslenabled' => is_https($_SERVER), + ]; $pluginManager->executeHooks('render_tools', $data); foreach ($data as $key => $value) { @@ -1153,6 +1165,7 @@ function renderPage($conf, $pluginManager, $LINKSDB) $conf->set('api.secret', escape($_POST['apiSecret'])); try { $conf->write(isLoggedIn()); + $history->updateSettings(); invalidateCaches($conf->get('resource.page_cache')); } catch(Exception $e) { @@ -1174,9 +1187,12 @@ function renderPage($conf, $pluginManager, $LINKSDB) $PAGE->assign('theme', $conf->get('resource.theme')); $PAGE->assign('theme_available', ThemeUtils::getThemes($conf->get('resource.raintpl_tpl'))); $PAGE->assign('redirector', $conf->get('redirector.url')); - list($timezone_form, $timezone_js) = generateTimeZoneForm($conf->get('general.timezone')); - $PAGE->assign('timezone_form', $timezone_form); - $PAGE->assign('timezone_js',$timezone_js); + list($continents, $cities) = generateTimeZoneData( + timezone_identifiers_list(), + $conf->get('general.timezone') + ); + $PAGE->assign('continents', $continents); + $PAGE->assign('cities', $cities); $PAGE->assign('private_links_default', $conf->get('privacy.default_private_links', false)); $PAGE->assign('session_protection_disabled', $conf->get('security.session_protection_disabled', false)); $PAGE->assign('enable_rss_permalinks', $conf->get('feed.rss_permalinks', false)); @@ -1193,7 +1209,7 @@ function renderPage($conf, $pluginManager, $LINKSDB) if ($targetPage == Router::$PAGE_CHANGETAG) { if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) { - $PAGE->assign('tags', $LINKSDB->allTags()); + $PAGE->assign('fromtag', ! empty($_GET['fromtag']) ? escape($_GET['fromtag']) : ''); $PAGE->renderPage('changetag'); exit; } @@ -1202,39 +1218,18 @@ function renderPage($conf, $pluginManager, $LINKSDB) die('Wrong token.'); } - // Delete a tag: - if (isset($_POST['deletetag']) && !empty($_POST['fromtag'])) { - $needle = trim($_POST['fromtag']); - // True for case-sensitive tag search. - $linksToAlter = $LINKSDB->filterSearch(array('searchtags' => $needle), true); - foreach($linksToAlter as $key=>$value) - { - $tags = explode(' ',trim($value['tags'])); - unset($tags[array_search($needle,$tags)]); // Remove tag. - $value['tags']=trim(implode(' ',$tags)); - $LINKSDB[$key]=$value; - } - $LINKSDB->save($conf->get('resource.page_cache')); - echo ''; - exit; - } - - // Rename a tag: - if (isset($_POST['renametag']) && !empty($_POST['fromtag']) && !empty($_POST['totag'])) { - $needle = trim($_POST['fromtag']); - // True for case-sensitive tag search. - $linksToAlter = $LINKSDB->filterSearch(array('searchtags' => $needle), true); - foreach($linksToAlter as $key=>$value) { - $tags = preg_split('/\s+/', trim($value['tags'])); - // Replace tags value. - $tags[array_search($needle, $tags)] = trim($_POST['totag']); - $value['tags'] = implode(' ', array_unique($tags)); - $LINKSDB[$key] = $value; - } - $LINKSDB->save($conf->get('resource.page_cache')); // Save to disk. - echo ''; - exit; - } + $alteredLinks = $LINKSDB->renameTag(escape($_POST['fromtag']), escape($_POST['totag'])); + $LINKSDB->save($conf->get('resource.page_cache')); + foreach ($alteredLinks as $link) { + $history->updateLink($link); + } + $delete = empty($_POST['totag']); + $redirect = $delete ? 'do=changetag' : 'searchtags='. urlencode(escape($_POST['totag'])); + $alert = $delete + ? sprintf(t('The tag was removed from %d links.'), count($alteredLinks)) + : sprintf(t('The tag was renamed in %d links.'), count($alteredLinks)); + echo ''; + exit; } // -------- User wants to add a link without using the bookmarklet: Show form. @@ -1257,18 +1252,20 @@ function renderPage($conf, $pluginManager, $LINKSDB) // Linkdate is kept here to: // - use the same permalink for notes as they're displayed when creating them // - let users hack creation date of their posts - // See: https://github.com/shaarli/Shaarli/wiki/Datastore-hacks#changing-the-timestamp-for-a-link + // See: https://shaarli.readthedocs.io/en/master/Various-hacks/#changing-the-timestamp-for-a-shaare $linkdate = escape($_POST['lf_linkdate']); if (isset($LINKSDB[$id])) { // Edit $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate); $updated = new DateTime(); $shortUrl = $LINKSDB[$id]['shorturl']; + $new = false; } else { // New link $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate); $updated = null; $shortUrl = link_small_hash($created, $id); + $new = true; } // Remove multiple spaces. @@ -1278,13 +1275,10 @@ function renderPage($conf, $pluginManager, $LINKSDB) // Remove duplicates. $tags = implode(' ', array_unique(explode(' ', $tags))); - $url = trim($_POST['lf_url']); - if (! startsWith($url, 'http:') && ! startsWith($url, 'https:') - && ! startsWith($url, 'ftp:') && ! startsWith($url, 'magnet:') - && ! startsWith($url, '?') && ! startsWith($url, 'javascript:') - ) { - $url = 'http://' . $url; + if (empty(trim($_POST['lf_url']))) { + $_POST['lf_url'] = '?' . smallHash($linkdate . $id); } + $url = whitelist_protocols(trim($_POST['lf_url']), $conf->get('security.allowed_protocols')); $link = array( 'id' => $id, @@ -1307,6 +1301,11 @@ function renderPage($conf, $pluginManager, $LINKSDB) $LINKSDB[$id] = $link; $LINKSDB->save($conf->get('resource.page_cache')); + if ($new) { + $history->addLink($link); + } else { + $history->updateLink($link); + } // If we are called from the bookmarklet, we must close the popup: if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { @@ -1344,19 +1343,30 @@ function renderPage($conf, $pluginManager, $LINKSDB) // -------- User clicked the "Delete" button when editing a link: Delete link from database. if ($targetPage == Router::$PAGE_DELETELINK) { - // We do not need to ask for confirmation: - // - confirmation is handled by JavaScript - // - we are protected from XSRF by the token. - if (! tokenOk($_GET['token'])) { die('Wrong token.'); } - $id = intval(escape($_GET['lf_linkdate'])); - $link = $LINKSDB[$id]; - $pluginManager->executeHooks('delete_link', $link); - unset($LINKSDB[$id]); + $ids = trim($_GET['lf_linkdate']); + if (strpos($ids, ' ') !== false) { + // multiple, space-separated ids provided + $ids = array_values(array_filter(preg_split('/\s+/', escape($ids)))); + } else { + // only a single id provided + $ids = [$ids]; + } + // assert at least one id is given + if(!count($ids)){ + die('no id provided'); + } + foreach ($ids as $id) { + $id = (int) escape($id); + $link = $LINKSDB[$id]; + $pluginManager->executeHooks('delete_link', $link); + unset($LINKSDB[$id]); + } $LINKSDB->save($conf->get('resource.page_cache')); // save to disk + $history->deleteLink($link); // If we are called from the bookmarklet, we must close the popup: if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo ''; exit; } @@ -1386,7 +1396,7 @@ function renderPage($conf, $pluginManager, $LINKSDB) 'link' => $link, 'link_is_new' => false, 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''), - 'tags' => $LINKSDB->allTags(), + 'tags' => $LINKSDB->linksCountPerTag(), ); $pluginManager->executeHooks('render_editlink', $data); @@ -1433,7 +1443,7 @@ function renderPage($conf, $pluginManager, $LINKSDB) if ($url == '') { $url = '?' . smallHash($linkdate . $LINKSDB->getNextId()); - $title = 'Note: '; + $title = $conf->get('general.default_note_title', 'Note: '); } $url = escape($url); $title = escape($title); @@ -1444,7 +1454,7 @@ function renderPage($conf, $pluginManager, $LINKSDB) 'url' => $url, 'description' => $description, 'tags' => $tags, - 'private' => $private + 'private' => $private, ); } else { $link['linkdate'] = $link['created']->format(LinkDB::LINK_DATE_FORMAT); @@ -1455,7 +1465,7 @@ function renderPage($conf, $pluginManager, $LINKSDB) 'link_is_new' => $link_is_new, 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''), 'source' => (isset($_GET['source']) ? $_GET['source'] : ''), - 'tags' => $LINKSDB->allTags(), + 'tags' => $LINKSDB->linksCountPerTag(), 'default_private_links' => $conf->get('privacy.default_private_links', false), ); $pluginManager->executeHooks('render_editlink', $data); @@ -1517,7 +1527,22 @@ function renderPage($conf, $pluginManager, $LINKSDB) if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) { // Show import dialog - $PAGE->assign('maxfilesize', getMaxFileSize()); + $PAGE->assign( + 'maxfilesize', + get_max_upload_size( + ini_get('post_max_size'), + ini_get('upload_max_filesize'), + false + ) + ); + $PAGE->assign( + 'maxfilesizeHuman', + get_max_upload_size( + ini_get('post_max_size'), + ini_get('upload_max_filesize'), + true + ) + ); $PAGE->renderPage('import'); exit; } @@ -1527,7 +1552,7 @@ function renderPage($conf, $pluginManager, $LINKSDB) // The file is too big or some form field may be missing. echo ''; exit; @@ -1539,7 +1564,8 @@ function renderPage($conf, $pluginManager, $LINKSDB) $_POST, $_FILES, $LINKSDB, - $conf + $conf, + $history ); echo ''; @@ -1579,6 +1605,7 @@ function renderPage($conf, $pluginManager, $LINKSDB) $conf->set('general.enabled_plugins', save_plugin_config($_POST)); } $conf->write(isLoggedIn()); + $history->updateSettings(); } catch (Exception $e) { error_log( @@ -1594,6 +1621,13 @@ function renderPage($conf, $pluginManager, $LINKSDB) exit; } + // Get a fresh token + if ($targetPage == Router::$GET_TOKEN) { + header('Content-Type:text/plain'); + echo getToken($conf); + exit; + } + // -------- Otherwise, simply display search form and links: showLinkList($PAGE, $LINKSDB, $conf, $pluginManager); exit; @@ -1611,7 +1645,15 @@ function renderPage($conf, $pluginManager, $LINKSDB) function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) { // Used in templates - $searchtags = !empty($_GET['searchtags']) ? escape(normalize_spaces($_GET['searchtags'])) : ''; + if (isset($_GET['searchtags'])) { + if (! empty($_GET['searchtags'])) { + $searchtags = escape(normalize_spaces($_GET['searchtags'])); + } else { + $searchtags = false; + } + } else { + $searchtags = ''; + } $searchterm = !empty($_GET['searchterm']) ? escape(normalize_spaces($_GET['searchterm'])) : ''; // Smallhash filter @@ -1626,7 +1668,11 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) } else { // Filter links according search parameters. $visibility = ! empty($_SESSION['privateonly']) ? 'private' : 'all'; - $linksToDisplay = $LINKSDB->filterSearch($_GET, false, $visibility); + $request = [ + 'searchtags' => $searchtags, + 'searchterm' => $searchterm, + ]; + $linksToDisplay = $LINKSDB->filterSearch($request, false, $visibility, !empty($_SESSION['untaggedonly'])); } // ---- Handle paging. @@ -1673,7 +1719,7 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) } // Compute paging navigation - $searchtagsUrl = empty($searchtags) ? '' : '&searchtags=' . urlencode($searchtags); + $searchtagsUrl = $searchtags === '' ? '' : '&searchtags=' . urlencode($searchtags); $searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm); $previous_page_url = ''; if ($i != count($keys)) { @@ -1696,7 +1742,6 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) 'visibility' => ! empty($_SESSION['privateonly']) ? 'private' : '', 'redirector' => $conf->get('redirector.url'), // Optional redirector URL. 'links' => $linkDisp, - 'tags' => $LINKSDB->allTags(), ); // If there is only a single link, we change on-the-fly the title of the page. @@ -1982,16 +2027,10 @@ function install($conf) exit; } - // Display config form: - list($timezone_form, $timezone_js) = generateTimeZoneForm(); - $timezone_html = ''; - if ($timezone_form != '') { - $timezone_html = 'Timezone:'.$timezone_form.''; - } - $PAGE = new PageBuilder($conf); - $PAGE->assign('timezone_html',$timezone_html); - $PAGE->assign('timezone_js',$timezone_js); + list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get()); + $PAGE->assign('continents', $continents); + $PAGE->assign('cities', $cities); $PAGE->renderPage('install'); exit; } @@ -2227,6 +2266,12 @@ if (!isset($_SESSION['LINKS_PER_PAGE'])) { $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20); } +try { + $history = new History($conf->get('resource.history')); +} catch(Exception $e) { + die($e->getMessage()); +} + $linkDb = new LinkDB( $conf->get('resource.datastore'), isLoggedIn(), @@ -2238,6 +2283,7 @@ $linkDb = new LinkDB( $container = new \Slim\Container(); $container['conf'] = $conf; $container['plugins'] = $pluginManager; +$container['history'] = $history; $app = new \Slim\App($container); // REST API routes @@ -2248,6 +2294,7 @@ $app->group('/api/v1', function() { $this->post('/links', '\Shaarli\Api\Controllers\Links:postLink')->setName('postLink'); $this->put('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:putLink')->setName('putLink'); $this->delete('/links/{id:[\d]+}', '\Shaarli\Api\Controllers\Links:deleteLink')->setName('deleteLink'); + $this->get('/history', '\Shaarli\Api\Controllers\History:getHistory')->setName('getHistory'); })->add('\Shaarli\Api\ApiMiddleware'); $response = $app->run(true); @@ -2256,7 +2303,7 @@ $response = $app->run(true); if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) { // We use UTF-8 for proper international characters handling. header('Content-Type: text/html; charset=utf-8'); - renderPage($conf, $pluginManager, $linkDb); + renderPage($conf, $pluginManager, $linkDb, $history); } else { $app->respond($response); }