X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=index.php;h=cf85197a375cb384e5dfb2b1820d6697a85e4441;hb=c8d96b4729a96ff2321862ca13a727658860e7a5;hp=d425afda8014b5495b86a2db116e451e11463388;hpb=9646b7da22c4c6f3419bfe51431720dd622374d8;p=github%2Fshaarli%2FShaarli.git diff --git a/index.php b/index.php index d425afda..c2552ce8 100644 --- a/index.php +++ b/index.php @@ -1,8 +1,6 @@ /shaarli/ define('WEB_PATH', substr($_SERVER['REQUEST_URI'], 0, 1+strrpos($_SERVER['REQUEST_URI'], '/', 0))); @@ -44,15 +41,30 @@ error_reporting(E_ALL^E_WARNING); //error_reporting(-1); +// 3rd-party libraries +if (! file_exists(__DIR__ . '/vendor/autoload.php')) { + header('Content-Type: text/plain; charset=utf-8'); + echo "Error: missing Composer configuration\n\n" + ."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://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'; +require_once __DIR__ . '/vendor/autoload.php'; + // Shaarli library require_once 'application/ApplicationUtils.php'; require_once 'application/Cache.php'; require_once 'application/CachedPage.php'; -require_once 'application/config/ConfigManager.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'; require_once 'application/LinkFilter.php'; require_once 'application/LinkUtils.php'; @@ -64,17 +76,20 @@ require_once 'application/Utils.php'; require_once 'application/PluginManager.php'; require_once 'application/Router.php'; require_once 'application/Updater.php'; -require_once 'inc/rain.tpl.class.php'; +use \Shaarli\ThemeUtils; +use \Shaarli\Config\ConfigManager; // Ensure the PHP version is supported try { - ApplicationUtils::checkPHPVersion('5.3', PHP_VERSION); + ApplicationUtils::checkPHPVersion('5.5', PHP_VERSION); } catch(Exception $exc) { header('Content-Type: text/plain; charset=utf-8'); echo $exc->getMessage(); exit; } +define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE)); + // Force cookie path (but do not change lifetime) $cookie = session_get_cookie_params(); $cookiedir = ''; @@ -108,7 +123,7 @@ if (isset($_COOKIE['shaarli']) && !is_session_id_valid($_COOKIE['shaarli'])) { $conf = new ConfigManager(); $conf->setEmpty('general.timezone', date_default_timezone_get()); $conf->setEmpty('general.title', 'Shared links on '. escape(index_url($_SERVER))); -RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl'); // template directory +RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory $pluginManager = new PluginManager($conf); @@ -118,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"); @@ -161,7 +167,6 @@ define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['R if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']); } -header('Content-Type: text/html; charset=utf-8'); // We use UTF-8 for proper international characters handling. /** * Checking session state (i.e. is the user still logged in) @@ -172,65 +177,44 @@ header('Content-Type: text/html; charset=utf-8'); // We use UTF-8 for proper int */ 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']) - || ($conf->get('security.session_protection_disabled') == false && $_SESSION['ip'] != allIPs()) + 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 @@ -252,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. } /** @@ -272,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; } @@ -294,6 +278,7 @@ function logout() { unset($_SESSION['ip']); unset($_SESSION['username']); unset($_SESSION['privateonly']); + unset($_SESSION['untaggedonly']); } setcookie('shaarli_staySignedIn', FALSE, 0, WEB_PATH); } @@ -318,8 +303,17 @@ include $conf->get('resource.ban_file', 'data/ipbans.php'); function ban_loginFailed($conf) { $ip = $_SERVER['REMOTE_ADDR']; + $trusted = $conf->get('security.trusted_proxies', array()); + if (in_array($ip, $trusted)) { + $ip = getIpAddressFromProxy($_SERVER, $trusted); + if (!$ip) { + return; + } + } $gb = $GLOBALS['IPBANS']; - if (!isset($gb['FAILURES'][$ip])) $gb['FAILURES'][$ip]=0; + if (! isset($gb['FAILURES'][$ip])) { + $gb['FAILURES'][$ip]=0; + } $gb['FAILURES'][$ip]++; if ($gb['FAILURES'][$ip] > ($conf->get('security.ban_after') - 1)) { @@ -391,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 @@ -410,7 +405,7 @@ if (isset($_POST['login'])) // Optional redirect after login: if (isset($_GET['post'])) { $uri = '?post='. urlencode($_GET['post']); - foreach (array('description', 'source', 'title') as $param) { + foreach (array('description', 'source', 'title', 'tags') as $param) { if (!empty($_GET[$param])) { $uri .= '&'.$param.'='.urlencode($_GET[$param]); } @@ -439,7 +434,7 @@ if (isset($_POST['login'])) $redir = '&username='. $_POST['login']; if (isset($_GET['post'])) { $redir .= '&post=' . urlencode($_GET['post']); - foreach (array('description', 'source', 'title') as $param) { + foreach (array('description', 'source', 'title', 'tags') as $param) { if (!empty($_GET[$param])) { $redir .= '&' . $param . '=' . urlencode($_GET[$param]); } @@ -450,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...). @@ -541,24 +508,19 @@ function showDailyRSS($conf) { ); /* Some Shaarlies may have very few links, so we need to look - back in time (rsort()) until we have enough days ($nb_of_days). + back in time until we have enough days ($nb_of_days). */ - $linkdates = array(); - foreach ($LINKSDB as $linkdate => $value) { - $linkdates[] = $linkdate; - } - rsort($linkdates); $nb_of_days = 7; // We take 7 days. $today = date('Ymd'); $days = array(); - foreach ($linkdates as $linkdate) { - $day = substr($linkdate, 0, 8); // Extract day (without time) - if (strcmp($day,$today) < 0) { + foreach ($LINKSDB as $link) { + $day = $link['created']->format('Ymd'); // Extract day (without time) + if (strcmp($day, $today) < 0) { if (empty($days[$day])) { $days[$day] = array(); } - $days[$day][] = $linkdate; + $days[$day][] = $link; } if (count($days) > $nb_of_days) { @@ -578,26 +540,18 @@ function showDailyRSS($conf) { echo ''. $pageaddr .''. PHP_EOL; // For each day. - foreach ($days as $day => $linkdates) { + foreach ($days as $day => $links) { $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000'); $absurl = escape(index_url($_SERVER).'?do=daily&day='.$day); // Absolute URL of the corresponding "Daily" page. - // Build the HTML body of this RSS entry. - $html = ''; - $href = ''; - $links = array(); - // We pre-format some fields for proper output. - foreach ($linkdates as $linkdate) { - $l = $LINKSDB[$linkdate]; - $l['formatedDescription'] = format_description($l['description'], $conf->get('redirector.url')); - $l['thumbnail'] = thumbnail($conf, $l['url']); - $l_date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $l['linkdate']); - $l['timestamp'] = $l_date->getTimestamp(); - if (startsWith($l['url'], '?')) { - $l['url'] = index_url($_SERVER) . $l['url']; // make permalink URL absolute + foreach ($links as &$link) { + $link['formatedDescription'] = format_description($link['description'], $conf->get('redirector.url')); + $link['thumbnail'] = thumbnail($conf, $link['url']); + $link['timestamp'] = $link['created']->getTimestamp(); + if (startsWith($link['url'], '?')) { + $link['url'] = index_url($_SERVER) . $link['url']; // make permalink URL absolute } - $links[$linkdate] = $l; } // Then build the HTML for this day: @@ -608,7 +562,7 @@ function showDailyRSS($conf) { $tpl->assign('links', $links); $tpl->assign('rssdate', escape($dayDate->format(DateTime::RSS))); $tpl->assign('hide_timestamps', $conf->get('privacy.hide_timestamps', false)); - $html = $tpl->draw('dailyrss', $return_string=true); + $html = $tpl->draw('dailyrss', true); echo $html . PHP_EOL; } @@ -629,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) { @@ -651,16 +614,13 @@ 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; $linksToDisplay[$key]['formatedDescription'] = format_description($link['description'], $conf->get('redirector.url')); $linksToDisplay[$key]['thumbnail'] = thumbnail($conf, $link['url']); - $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']); - $linksToDisplay[$key]['timestamp'] = $date->getTimestamp(); + $linksToDisplay[$key]['timestamp'] = $link['created']->getTimestamp(); } /* We need to spread the articles on 3 columns. @@ -668,28 +628,31 @@ 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'); $data = array( + 'pagetitle' => $conf->get('general.title') .' - '. format_date($dayDate, false), 'linksToDisplay' => $linksToDisplay, 'cols' => $columns, 'day' => $dayDate->getTimestamp(), + 'dayDate' => $dayDate, 'previousday' => $previousday, 'nextday' => $nextday, ); @@ -722,17 +685,11 @@ 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) +function renderPage($conf, $pluginManager, $LINKSDB, $history) { - $LINKSDB = new LinkDB( - $conf->get('resource.datastore'), - isLoggedIn(), - $conf->get('privacy.hide_public_links'), - $conf->get('redirector.url'), - $conf->get('redirector.encode_url') - ); - $updater = new Updater( read_updates_file($conf->get('resource.updates')), $LINKSDB, @@ -752,14 +709,32 @@ function renderPage($conf, $pluginManager) 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()); // Determine which page will be rendered. $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( @@ -783,12 +758,12 @@ function renderPage($conf, $pluginManager) if ($targetPage == Router::$PAGE_LOGIN) { if ($conf->get('security.open_shaarli')) { header('Location: ?'); exit; } // No need to login for open Shaarli - $token=''; if (ban_canLogin($conf)) $token=getToken($conf); // Do not waste token generation if not useful. - $PAGE->assign('token',$token); if (isset($_GET['username'])) { $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; } @@ -811,7 +786,7 @@ function renderPage($conf, $pluginManager) // Get only links which have a thumbnail. foreach($links as $link) { - $permalink='?'.escape(smallhash($link['linkdate'])); + $permalink='?'.$link['shorturl']; $thumb=lazyThumbnail($conf, $link['url'],$permalink); if ($thumb!='') // Only output links which have a thumbnail. { @@ -836,7 +811,9 @@ function renderPage($conf, $pluginManager) // -------- 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. @@ -845,20 +822,13 @@ function renderPage($conf, $pluginManager) $maxcount = max($maxcount, $value); } - // Sort tags alphabetically: case insensitive, support locale if avalaible. - 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. @@ -870,6 +840,7 @@ function renderPage($conf, $pluginManager) } $data = array( + 'search_tags' => implode(' ', $filteringTags), 'tags' => $tagList, ); $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn())); @@ -878,7 +849,37 @@ function renderPage($conf, $pluginManager) $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(' ', $filteringTags), + 'tags' => $tags, + ]; + $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]); + + foreach ($data as $key => $value) { + $PAGE->assign($key, $value); + } + + $PAGE->renderPage('tag.list'); exit; } @@ -910,10 +911,6 @@ function renderPage($conf, $pluginManager) $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0))); $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !isLoggedIn()); $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks')); - $pshUrl = $conf->get('config.PUBSUBHUB_URL'); - if (!empty($pshUrl)) { - $feedGenerator->setPubsubhubUrl($pshUrl); - } $data = $feedGenerator->buildData(); // Process plugin hook. @@ -930,7 +927,7 @@ function renderPage($conf, $pluginManager) exit; } - // Display openseach plugin (XML) + // Display opensearch plugin (XML) if ($targetPage == Router::$PAGE_OPENSEARCH) { header('Content-Type: application/xml; charset=utf-8'); $PAGE->assign('serverurl', index_url($_SERVER)); @@ -1015,7 +1012,12 @@ function renderPage($conf, $pluginManager) $_SESSION['LINKS_PER_PAGE']=abs(intval($_GET['linksperpage'])); } - header('Location: '. generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('linksperpage'))); + if (! empty($_SERVER['HTTP_REFERER'])) { + $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('linksperpage')); + } else { + $location = '?'; + } + header('Location: '. $location); exit; } @@ -1027,7 +1029,25 @@ function renderPage($conf, $pluginManager) unset($_SESSION['privateonly']); // See all links } - header('Location: '. generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('privateonly'))); + if (! empty($_SERVER['HTTP_REFERER'])) { + $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('privateonly')); + } else { + $location = '?'; + } + header('Location: '. $location); + 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; } @@ -1038,7 +1058,13 @@ function renderPage($conf, $pluginManager) // Show login screen, then redirect to ?post=... if (isset($_GET['post'])) { - header('Location: ?do=login&post='.urlencode($_GET['post']).(!empty($_GET['title'])?'&title='.urlencode($_GET['title']):'').(!empty($_GET['description'])?'&description='.urlencode($_GET['description']):'').(!empty($_GET['source'])?'&source='.urlencode($_GET['source']):'')); // Redirect to login page, then back to post link. + header( // Redirect to login page, then back to post link. + 'Location: ?do=login&post='.urlencode($_GET['post']). + (!empty($_GET['title'])?'&title='.urlencode($_GET['title']):''). + (!empty($_GET['description'])?'&description='.urlencode($_GET['description']):''). + (!empty($_GET['tags'])?'&tags='.urlencode($_GET['tags']):''). + (!empty($_GET['source'])?'&source='.urlencode($_GET['source']):'') + ); exit; } @@ -1056,9 +1082,10 @@ function renderPage($conf, $pluginManager) // -------- Display the Tools menu if requested (import/export/bookmarklet...) if ($targetPage == Router::$PAGE_TOOLS) { - $data = array( + $data = [ 'pageabsaddr' => index_url($_SERVER), - ); + 'sslenabled' => is_https($_SERVER), + ]; $pluginManager->executeHooks('render_tools', $data); foreach ($data as $key => $value) { @@ -1105,7 +1132,6 @@ function renderPage($conf, $pluginManager) } else // show the change password form. { - $PAGE->assign('token',getToken($conf)); $PAGE->renderPage('changepassword'); exit; } @@ -1128,14 +1154,19 @@ function renderPage($conf, $pluginManager) $conf->set('general.timezone', $tz); $conf->set('general.title', escape($_POST['title'])); $conf->set('general.header_link', escape($_POST['titleLink'])); + $conf->set('resource.theme', escape($_POST['theme'])); $conf->set('redirector.url', escape($_POST['redirector'])); $conf->set('security.session_protection_disabled', !empty($_POST['disablesessionprotection'])); $conf->set('privacy.default_private_links', !empty($_POST['privateLinkByDefault'])); $conf->set('feed.rss_permalinks', !empty($_POST['enableRssPermalinks'])); $conf->set('updates.check_updates', !empty($_POST['updateCheck'])); $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks'])); + $conf->set('api.enabled', !empty($_POST['enableApi'])); + $conf->set('api.secret', escape($_POST['apiSecret'])); try { $conf->write(isLoggedIn()); + $history->updateSettings(); + invalidateCaches($conf->get('resource.page_cache')); } catch(Exception $e) { error_log( @@ -1152,17 +1183,23 @@ function renderPage($conf, $pluginManager) } else // Show the configuration form. { - $PAGE->assign('token',getToken($conf)); $PAGE->assign('title', $conf->get('general.title')); + $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)); $PAGE->assign('enable_update_check', $conf->get('updates.check_updates', true)); $PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false)); + $PAGE->assign('api_enabled', $conf->get('api.enabled', true)); + $PAGE->assign('api_secret', $conf->get('api.secret')); $PAGE->renderPage('configure'); exit; } @@ -1172,8 +1209,7 @@ function renderPage($conf, $pluginManager) if ($targetPage == Router::$PAGE_CHANGETAG) { if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) { - $PAGE->assign('token', getToken($conf)); - $PAGE->assign('tags', $LINKSDB->allTags()); + $PAGE->assign('fromtag', ! empty($_GET['fromtag']) ? escape($_GET['fromtag']) : ''); $PAGE->renderPage('changetag'); exit; } @@ -1182,39 +1218,18 @@ function renderPage($conf, $pluginManager) 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->savedb($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 = explode(' ',trim($value['tags'])); - $tags[array_search($needle,$tags)] = trim($_POST['totag']); // Replace tags value. - $value['tags']=trim(implode(' ',$tags)); - $LINKSDB[$key]=$value; - } - $LINKSDB->savedb($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. @@ -1227,13 +1242,32 @@ function renderPage($conf, $pluginManager) // -------- User clicked the "Save" button when editing a link: Save link to database. if (isset($_POST['save_edit'])) { - $linkdate = $_POST['lf_linkdate']; - $updated = isset($LINKSDB[$linkdate]) ? strval(date('Ymd_His')) : false; - // Go away! if (! tokenOk($_POST['token'])) { die('Wrong token.'); } + + // lf_id should only be present if the link exists. + $id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : $LINKSDB->getNextId(); + // 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://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. $tags = trim(preg_replace('/\s\s+/', ' ', $_POST['lf_tags'])); // Remove first '-' char in tags. @@ -1241,23 +1275,23 @@ function renderPage($conf, $pluginManager) // 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, 'title' => trim($_POST['lf_title']), 'url' => $url, 'description' => $_POST['lf_description'], 'private' => (isset($_POST['lf_private']) ? 1 : 0), - 'linkdate' => $linkdate, + 'created' => $created, 'updated' => $updated, - 'tags' => str_replace(',', ' ', $tags) + 'tags' => str_replace(',', ' ', $tags), + 'shorturl' => $shortUrl, ); + // If title is empty, use the URL as title. if ($link['title'] == '') { $link['title'] = $link['url']; @@ -1265,9 +1299,13 @@ function renderPage($conf, $pluginManager) $pluginManager->executeHooks('save_link', $link); - $LINKSDB[$linkdate] = $link; - $LINKSDB->savedb($conf->get('resource.page_cache')); - pubsubhub($conf); + $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')) { @@ -1278,7 +1316,7 @@ function renderPage($conf, $pluginManager) $returnurl = !empty($_POST['returnurl']) ? $_POST['returnurl'] : '?'; $location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link')); // Scroll to the link which has been edited. - $location .= '#' . smallHash($_POST['lf_linkdate']); + $location .= '#' . $link['shorturl']; // After saving the link, redirect to the page the user was on. header('Location: '. $location); exit; @@ -1287,56 +1325,60 @@ function renderPage($conf, $pluginManager) // -------- User clicked the "Cancel" button when editing a link. if (isset($_POST['cancel_edit'])) { + $id = isset($_POST['lf_id']) ? (int) escape($_POST['lf_id']) : false; + if (! isset($LINKSDB[$id])) { + header('Location: ?'); + } // If we are called from the bookmarklet, we must close the popup: if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo ''; exit; } + $link = $LINKSDB[$id]; $returnurl = ( isset($_POST['returnurl']) ? $_POST['returnurl'] : '?' ); - $returnurl .= '#'.smallHash($_POST['lf_linkdate']); // Scroll to the link which has been edited. + // Scroll to the link which has been edited. + $returnurl .= '#'. $link['shorturl']; $returnurl = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link')); header('Location: '.$returnurl); // After canceling, redirect to the page the user was on. exit; } // -------- User clicked the "Delete" button when editing a link: Delete link from database. - if (isset($_POST['delete_link'])) + if ($targetPage == Router::$PAGE_DELETELINK) { - if (!tokenOk($_POST['token'])) die('Wrong token.'); - // We do not need to ask for confirmation: - // - confirmation is handled by JavaScript - // - we are protected from XSRF by the token. - $linkdate=$_POST['lf_linkdate']; - - $pluginManager->executeHooks('delete_link', $LINKSDB[$linkdate]); + if (! tokenOk($_GET['token'])) { + die('Wrong token.'); + } - unset($LINKSDB[$linkdate]); - $LINKSDB->savedb('resource.page_cache'); // save to disk + $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; } - // Pick where we're going to redirect - // ============================================================= - // Basically, we can't redirect to where we were previously if it was a permalink - // or an edit_link, because it would 404. - // Cases: - // - / : nothing in $_GET, redirect to self - // - /?page : redirect to self - // - /?searchterm : redirect to self (there might be other links) - // - /?searchtags : redirect to self - // - /permalink : redirect to / (the link does not exist anymore) - // - /?edit_link : redirect to / (the link does not exist anymore) - // PHP treats the permalink as a $_GET variable, so we need to check if every condition for self - // redirect is not satisfied, and only then redirect to / - $location = "?"; - // Self redirection - if (count($_GET) == 0 - || isset($_GET['page']) - || isset($_GET['searchterm']) - || isset($_GET['searchtags']) - ) { - if (isset($_POST['returnurl'])) { - $location = $_POST['returnurl']; // Handle redirects given by the form - } else { - $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('delete_link')); - } + + $location = '?'; + if (isset($_SERVER['HTTP_REFERER'])) { + // Don't redirect to where we were previously if it was a permalink or an edit_link, because it would 404. + $location = generateLocation( + $_SERVER['HTTP_REFERER'], + $_SERVER['HTTP_HOST'], + ['delete_link', 'edit_link', $link['shorturl']] + ); } header('Location: ' . $location); // After deleting the link, redirect to appropriate location @@ -1346,14 +1388,15 @@ function renderPage($conf, $pluginManager) // -------- User clicked the "EDIT" button on a link: Display link edit form. if (isset($_GET['edit_link'])) { - $link = $LINKSDB[$_GET['edit_link']]; // Read database + $id = (int) escape($_GET['edit_link']); + $link = $LINKSDB[$id]; // Read database if (!$link) { header('Location: ?'); exit; } // Link not found in database. + $link['linkdate'] = $link['created']->format(LinkDB::LINK_DATE_FORMAT); $data = array( 'link' => $link, 'link_is_new' => false, - 'token' => getToken($conf), 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''), - 'tags' => $LINKSDB->allTags(), + 'tags' => $LINKSDB->linksCountPerTag(), ); $pluginManager->executeHooks('render_editlink', $data); @@ -1372,10 +1415,10 @@ function renderPage($conf, $pluginManager) $link_is_new = false; // Check if URL is not already in database (in this case, we will edit the existing link) $link = $LINKSDB->getLinkFromUrl($url); - if (!$link) + if (! $link) { $link_is_new = true; - $linkdate = strval(date('Ymd_His')); + $linkdate = strval(date(LinkDB::LINK_DATE_FORMAT)); // Get title if it was provided in URL (by the bookmarklet). $title = empty($_GET['title']) ? '' : escape($_GET['title']); // Get description if it was provided in URL (by the bookmarklet). [Bronco added that] @@ -1399,8 +1442,8 @@ function renderPage($conf, $pluginManager) } if ($url == '') { - $url = '?' . smallHash($linkdate); - $title = 'Note: '; + $url = '?' . smallHash($linkdate . $LINKSDB->getNextId()); + $title = $conf->get('general.default_note_title', 'Note: '); } $url = escape($url); $title = escape($title); @@ -1411,18 +1454,19 @@ function renderPage($conf, $pluginManager) 'url' => $url, 'description' => $description, 'tags' => $tags, - 'private' => $private + 'private' => $private, ); + } else { + $link['linkdate'] = $link['created']->format(LinkDB::LINK_DATE_FORMAT); } $data = array( 'link' => $link, 'link_is_new' => $link_is_new, - 'token' => getToken($conf), // XSRF protection. 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''), 'source' => (isset($_GET['source']) ? $_GET['source'] : ''), - 'tags' => $LINKSDB->allTags(), - 'default_private_links' => $conf->get('default_private_links', false), + 'tags' => $LINKSDB->linksCountPerTag(), + 'default_private_links' => $conf->get('privacy.default_private_links', false), ); $pluginManager->executeHooks('render_editlink', $data); @@ -1478,27 +1522,53 @@ function renderPage($conf, $pluginManager) exit; } - // -------- User is uploading a file for import - if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=upload')) - { - // If file is too big, some form field may be missing. - if (!isset($_POST['token']) || (!isset($_FILES)) || (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size']==0)) - { - $returnurl = ( empty($_SERVER['HTTP_REFERER']) ? '?' : $_SERVER['HTTP_REFERER'] ); - echo ''; + if ($targetPage == Router::$PAGE_IMPORT) { + // Upload a Netscape bookmark dump to import its contents + + if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) { + // Show import dialog + $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; } - if (!tokenOk($_POST['token'])) die('Wrong token.'); - importFile($LINKSDB); - exit; - } - // -------- Show upload/import dialog: - if ($targetPage == Router::$PAGE_IMPORT) - { - $PAGE->assign('token',getToken($conf)); - $PAGE->assign('maxfilesize',getMaxFileSize()); - $PAGE->renderPage('import'); + // Import bookmarks from an uploaded file + if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) { + // The file is too big or some form field may be missing. + echo ''; + exit; + } + if (! tokenOk($_POST['token'])) { + die('Wrong token.'); + } + $status = NetscapeBookmarkUtils::import( + $_POST, + $_FILES, + $LINKSDB, + $conf, + $history + ); + echo ''; exit; } @@ -1535,6 +1605,7 @@ function renderPage($conf, $pluginManager) $conf->set('general.enabled_plugins', save_plugin_config($_POST)); } $conf->write(isLoggedIn()); + $history->updateSettings(); } catch (Exception $e) { error_log( @@ -1550,100 +1621,18 @@ function renderPage($conf, $pluginManager) 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; } -/** - * Process the import file form. - * - * @param LinkDB $LINKSDB Loaded LinkDB instance. - * @param ConfigManager $conf Configuration Manager instance. - */ -function importFile($LINKSDB, $conf) -{ - if (!isLoggedIn()) { die('Not allowed.'); } - - $filename=$_FILES['filetoupload']['name']; - $filesize=$_FILES['filetoupload']['size']; - $data=file_get_contents($_FILES['filetoupload']['tmp_name']); - $private = (empty($_POST['private']) ? 0 : 1); // Should the links be imported as private? - $overwrite = !empty($_POST['overwrite']) ; // Should the imported links overwrite existing ones? - $import_count=0; - - // Sniff file type: - $type='unknown'; - if (startsWith($data,'')) $type='netscape'; // Netscape bookmark file (aka Firefox). - - // Then import the bookmarks. - if ($type=='netscape') - { - // This is a standard Netscape-style bookmark file. - // This format is supported by all browsers (except IE, of course), also Delicious, Diigo and others. - foreach(explode('
',$data) as $html) // explode is very fast - { - $link = array('linkdate'=>'','title'=>'','url'=>'','description'=>'','tags'=>'','private'=>0); - $d = explode('
',$html); - if (startsWith($d[0], '(.*?)!i',$d[0],$matches); $link['title'] = (isset($matches[1]) ? trim($matches[1]) : ''); // Get title - $link['title'] = html_entity_decode($link['title'],ENT_QUOTES,'UTF-8'); - preg_match_all('! ([A-Z_]+)=\"(.*?)"!i',$html,$matches,PREG_SET_ORDER); // Get all other attributes - $raw_add_date=0; - foreach($matches as $m) - { - $attr=$m[1]; $value=$m[2]; - if ($attr=='HREF') $link['url']=html_entity_decode($value,ENT_QUOTES,'UTF-8'); - elseif ($attr=='ADD_DATE') - { - $raw_add_date=intval($value); - if ($raw_add_date>30000000000) $raw_add_date/=1000; //If larger than year 2920, then was likely stored in milliseconds instead of seconds - } - elseif ($attr=='PRIVATE') $link['private']=($value=='0'?0:1); - elseif ($attr=='TAGS') $link['tags']=html_entity_decode(str_replace(',',' ',$value),ENT_QUOTES,'UTF-8'); - } - if ($link['url']!='') - { - if ($private==1) $link['private']=1; - $dblink = $LINKSDB->getLinkFromUrl($link['url']); // See if the link is already in database. - if ($dblink==false) - { // Link not in database, let's import it... - if (empty($raw_add_date)) $raw_add_date=time(); // In case of shitty bookmark file with no ADD_DATE - - // Make sure date/time is not already used by another link. - // (Some bookmark files have several different links with the same ADD_DATE) - // We increment date by 1 second until we find a date which is not used in DB. - // (so that links that have the same date/time are more or less kept grouped by date, but do not conflict.) - while (!empty($LINKSDB[date('Ymd_His',$raw_add_date)])) { $raw_add_date++; }// Yes, I know it's ugly. - $link['linkdate']=date('Ymd_His',$raw_add_date); - $LINKSDB[$link['linkdate']] = $link; - $import_count++; - } - else // Link already present in database. - { - if ($overwrite) - { // If overwrite is required, we import link data, except date/time. - $link['linkdate']=$dblink['linkdate']; - $LINKSDB[$link['linkdate']] = $link; - $import_count++; - } - } - - } - } - } - $LINKSDB->savedb($conf->get('resource.page_cache')); - - echo ''; - } - else - { - echo ''; - } -} - /** * Template for the list of links (