X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=index.php;h=baae41793ed579f95b4b1a19b163d1e9fd7ecc04;hb=340777ad3f0b4e798a3eedfb730d993972adbe3e;hp=600b2f5540242d24b81a8fb48e5c5e0574c7830a;hpb=1557cefbd76257ceb830f65806831b490faf0acc;p=github%2Fshaarli%2FShaarli.git diff --git a/index.php b/index.php index 600b2f55..baae4179 100644 --- a/index.php +++ b/index.php @@ -1,6 +1,6 @@ /shaarli/ define('WEB_PATH', substr($_SERVER["REQUEST_URI"], 0, 1+strrpos($_SERVER["REQUEST_URI"], '/', 0))); @@ -148,17 +155,21 @@ if (is_file($GLOBALS['config']['CONFIG_FILE'])) { require_once 'application/ApplicationUtils.php'; require_once 'application/Cache.php'; require_once 'application/CachedPage.php'; +require_once 'application/FeedBuilder.php'; require_once 'application/FileUtils.php'; require_once 'application/HttpUtils.php'; require_once 'application/LinkDB.php'; require_once 'application/LinkFilter.php'; require_once 'application/LinkUtils.php'; +require_once 'application/NetscapeBookmarkUtils.php'; +require_once 'application/PageBuilder.php'; require_once 'application/TimeZone.php'; require_once 'application/Url.php'; require_once 'application/Utils.php'; require_once 'application/Config.php'; require_once 'application/PluginManager.php'; require_once 'application/Router.php'; +require_once 'application/Updater.php'; // Ensure the PHP version is supported try { @@ -261,7 +272,10 @@ $GLOBALS['redirector'] = !empty($GLOBALS['redirector']) ? escape($GLOBALS['redir // a token depending of deployment salt, user password, and the current ip define('STAY_SIGNED_IN_TOKEN', sha1($GLOBALS['hash'].$_SERVER["REMOTE_ADDR"].$GLOBALS['salt'])); -autoLocale(); // Sniff browser language and set date format accordingly. +// Sniff browser language and set date format accordingly. +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. //================================================================================================== @@ -308,34 +322,6 @@ function setup_login_state() { } $userIsLoggedIn = setup_login_state(); - -// ----------------------------------------------------------------------------------------------- -// Log to text file -function logm($message) -{ - $t = strval(date('Y/m/d_H:i:s')).' - '.$_SERVER["REMOTE_ADDR"].' - '.strval($message)."\n"; - file_put_contents($GLOBALS['config']['LOG_FILE'], $t, FILE_APPEND); -} - -// ------------------------------------------------------------------------------------------ -// Sniff browser language to display dates in the right format automatically. -// (Note that is may not work on your server if the corresponding local is not installed.) -function autoLocale() -{ - $attempts = array('en_US'); // Default if browser does not send HTTP_ACCEPT_LANGUAGE - if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) // e.g. "fr,fr-fr;q=0.8,en;q=0.5,en-us;q=0.3" - { // (It's a bit crude, but it works very well. Preferred language is always presented first.) - if (preg_match('/([a-z]{2})-?([a-z]{2})?/i',$_SERVER['HTTP_ACCEPT_LANGUAGE'],$matches)) { - $loc = $matches[1] . (!empty($matches[2]) ? '_' . strtoupper($matches[2]) : ''); - $attempts = array($loc.'.UTF-8', $loc, str_replace('_', '-', $loc).'.UTF-8', str_replace('_', '-', $loc), - $loc . '_' . strtoupper($loc).'.UTF-8', $loc . '_' . strtoupper($loc), - $loc . '_' . $loc.'.UTF-8', $loc . '_' . $loc, $loc . '-' . strtoupper($loc).'.UTF-8', - $loc . '-' . strtoupper($loc), $loc . '-' . $loc.'.UTF-8', $loc . '-' . $loc); - } - } - setlocale(LC_TIME, $attempts); // LC_TIME = Set local for date/time format only. -} - // ------------------------------------------------------------------------------------------ // PubSubHubbub protocol support (if enabled) [UNTESTED] // (Source: http://aldarone.fr/les-flux-rss-shaarli-et-pubsubhubbub/ ) @@ -380,10 +366,10 @@ function check_auth($login,$password) if ($login==$GLOBALS['login'] && $hash==$GLOBALS['hash']) { // Login/password is correct. fillSessionInfo(); - logm('Login successful'); + logm($GLOBALS['config']['LOG_FILE'], $_SERVER['REMOTE_ADDR'], 'Login successful'); return True; } - logm('Login failed for user '.$login); + logm($GLOBALS['config']['LOG_FILE'], $_SERVER['REMOTE_ADDR'], 'Login failed for user '.$login); return False; } @@ -420,7 +406,7 @@ function ban_loginFailed() if ($gb['FAILURES'][$ip]>($GLOBALS['config']['BAN_AFTER']-1)) { $gb['BANS'][$ip]=time()+$GLOBALS['config']['BAN_DURATION']; - logm('IP address banned from login'); + logm($GLOBALS['config']['LOG_FILE'], $_SERVER['REMOTE_ADDR'], 'IP address banned from login'); } $GLOBALS['IPBANS'] = $gb; file_put_contents($GLOBALS['config']['IPBANS_FILENAME'], ""); @@ -444,7 +430,7 @@ function ban_canLogin() // User is banned. Check if the ban has expired: if ($gb['BANS'][$ip]<=time()) { // Ban expired, user can try to login again. - logm('Ban lifted.'); + logm($GLOBALS['config']['LOG_FILE'], $_SERVER['REMOTE_ADDR'], 'Ban lifted.'); unset($gb['FAILURES'][$ip]); unset($gb['BANS'][$ip]); file_put_contents($GLOBALS['config']['IPBANS_FILENAME'], ""); return true; // Ban has expired, user can login. @@ -480,7 +466,7 @@ if (isset($_POST['login'])) session_set_cookie_params(0,$cookiedir,$_SERVER['SERVER_NAME']); // 0 means "When browser closes" session_regenerate_id(true); } - + // Optional redirect after login: if (isset($_GET['post'])) { $uri = '?post='. urlencode($_GET['post']); @@ -501,7 +487,7 @@ if (isset($_POST['login'])) if (isset($_POST['returnurl'])) { // Prevent loops over login screen. if (strpos($_POST['returnurl'], 'do=login') === false) { - header('Location: '. escape($_POST['returnurl'])); + header('Location: '. generateLocation($_POST['returnurl'], $_SERVER['HTTP_HOST'])); exit; } } @@ -510,9 +496,9 @@ if (isset($_POST['login'])) else { ban_loginFailed(); - $redir = ''; + $redir = '&username='. $_POST['login']; if (isset($_GET['post'])) { - $redir = '?post=' . urlencode($_GET['post']); + $redir .= '&post=' . urlencode($_GET['post']); foreach (array('description', 'source', 'title') as $param) { if (!empty($_GET[$param])) { $redir .= '&' . $param . '=' . urlencode($_GET[$param]); @@ -552,33 +538,6 @@ function getMaxFileSize() return $maxsize; } -/* Converts a linkdate time (YYYYMMDD_HHMMSS) of an article to a timestamp (Unix epoch) - (used to build the ADD_DATE attribute in Netscape-bookmarks file) - PS: I could have used strptime(), but it does not exist on Windows. I'm too kind. */ -function linkdate2timestamp($linkdate) -{ - if(strcmp($linkdate, '_000000') !== 0 || !$linkdate){ - $Y=$M=$D=$h=$m=$s=0; - $r = sscanf($linkdate,'%4d%2d%2d_%2d%2d%2d',$Y,$M,$D,$h,$m,$s); - return mktime($h,$m,$s,$M,$D,$Y); - } - return time(); -} - -/* Converts a linkdate time (YYYYMMDD_HHMMSS) of an article to a RFC822 date. - (used to build the pubDate attribute in RSS feed.) */ -function linkdate2rfc822($linkdate) -{ - return date('r',linkdate2timestamp($linkdate)); // 'r' is for RFC822 date format. -} - -/* Converts a linkdate time (YYYYMMDD_HHMMSS) of an article to a ISO 8601 date. - (used to build the updated tags in ATOM feed.) */ -function linkdate2iso8601($linkdate) -{ - return date('c',linkdate2timestamp($linkdate)); // 'c' is for ISO 8601 date format. -} - // ------------------------------------------------------------------------------------------ // Token management for XSRF protection // Token should be used in any form which acts on data (create,update,delete,import...). @@ -604,304 +563,13 @@ function tokenOk($token) return false; // Wrong token, or already used. } -// ------------------------------------------------------------------------------------------ -/* This class is in charge of building the final page. - (This is basically a wrapper around RainTPL which pre-fills some fields.) - p = new pageBuilder; - p.assign('myfield','myvalue'); - p.renderPage('mytemplate'); - -*/ -class pageBuilder -{ - private $tpl; // RainTPL template - - function __construct() - { - $this->tpl = false; - } - - /** - * Initialize all default tpl tags. - */ - private function initialize() - { - $this->tpl = new RainTPL; - - try { - $version = ApplicationUtils::checkUpdate( - shaarli_version, - $GLOBALS['config']['UPDATECHECK_FILENAME'], - $GLOBALS['config']['UPDATECHECK_INTERVAL'], - $GLOBALS['config']['ENABLE_UPDATECHECK'], - isLoggedIn(), - $GLOBALS['config']['UPDATECHECK_BRANCH'] - ); - $this->tpl->assign('newVersion', escape($version)); - $this->tpl->assign('versionError', ''); - - } catch (Exception $exc) { - logm($exc->getMessage()); - $this->tpl->assign('newVersion', ''); - $this->tpl->assign('versionError', escape($exc->getMessage())); - } - - $this->tpl->assign('feedurl', escape(index_url($_SERVER))); - $searchcrits = ''; // Search criteria - if (!empty($_GET['searchtags'])) { - $searchcrits .= '&searchtags=' . urlencode($_GET['searchtags']); - } - elseif (!empty($_GET['searchterm'])) { - $searchcrits .= '&searchterm=' . urlencode($_GET['searchterm']); - } - $this->tpl->assign('searchcrits', $searchcrits); - $this->tpl->assign('source', index_url($_SERVER)); - $this->tpl->assign('version', shaarli_version); - $this->tpl->assign('scripturl', index_url($_SERVER)); - $this->tpl->assign('pagetitle', 'Shaarli'); - $this->tpl->assign('privateonly', !empty($_SESSION['privateonly'])); // Show only private links? - if (!empty($GLOBALS['title'])) { - $this->tpl->assign('pagetitle', $GLOBALS['title']); - } - if (!empty($GLOBALS['titleLink'])) { - $this->tpl->assign('titleLink', $GLOBALS['titleLink']); - } - if (!empty($GLOBALS['pagetitle'])) { - $this->tpl->assign('pagetitle', $GLOBALS['pagetitle']); - } - $this->tpl->assign('shaarlititle', empty($GLOBALS['title']) ? 'Shaarli': $GLOBALS['title']); - if (!empty($GLOBALS['plugin_errors'])) { - $this->tpl->assign('plugin_errors', $GLOBALS['plugin_errors']); - } - } - - // The following assign() method is basically the same as RainTPL (except that it's lazy) - public function assign($what,$where) - { - if ($this->tpl===false) $this->initialize(); // Lazy initialization - $this->tpl->assign($what,$where); - } - - // Render a specific page (using a template). - // e.g. pb.renderPage('picwall') - public function renderPage($page) - { - if ($this->tpl===false) $this->initialize(); // Lazy initialization - $this->tpl->draw($page); - } -} - -// ------------------------------------------------------------------------------------------ -// Output the last N links in RSS 2.0 format. -function showRSS() -{ - header('Content-Type: application/rss+xml; charset=utf-8'); - - // $usepermalink : If true, use permalink instead of final link. - // User just has to add 'permalink' in URL parameters. e.g. http://mysite.com/shaarli/?do=rss&permalinks - // Also enabled through a config option - $usepermalinks = isset($_GET['permalinks']) || !$GLOBALS['config']['ENABLE_RSS_PERMALINKS']; - - // Cache system - $query = $_SERVER["QUERY_STRING"]; - $cache = new CachedPage( - $GLOBALS['config']['PAGECACHE'], - page_url($_SERVER), - startsWith($query,'do=rss') && !isLoggedIn() - ); - $cached = $cache->cachedVersion(); - if (! empty($cached)) { - echo $cached; - exit; - } - - // If cached was not found (or not usable), then read the database and build the response: - $LINKSDB = new LinkDB( - $GLOBALS['config']['DATASTORE'], - isLoggedIn(), - $GLOBALS['config']['HIDE_PUBLIC_LINKS'], - $GLOBALS['redirector'] - ); - // Read links from database (and filter private links if user it not logged in). - - // Optionally filter the results: - if (!empty($_GET['searchterm'])) { - $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']); - } - elseif (!empty($_GET['searchtags'])) { - $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags'])); - } - else { - $linksToDisplay = $LINKSDB; - } - - $nblinksToDisplay = 50; // Number of links to display. - // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links. - if (!empty($_GET['nb'])) { - $nblinksToDisplay = $_GET['nb'] == 'all' ? count($linksToDisplay) : max(intval($_GET['nb']), 1); - } - - $pageaddr = escape(index_url($_SERVER)); - echo ''; - echo ''.$GLOBALS['title'].''.$pageaddr.''; - echo 'Shared linksen-en'.$pageaddr.''."\n\n"; - if (!empty($GLOBALS['config']['PUBSUBHUB_URL'])) - { - echo ''; - echo ''; - echo ''; - echo ''; - } - $i=0; - $keys=array(); foreach($linksToDisplay as $key=>$value) { $keys[]=$key; } // No, I can't use array_keys(). - while ($i<$nblinksToDisplay && $i'.$link['title'].''.$guid.''.$guid.''; - else - echo ''.$link['title'].''.$guid.''.$absurl.''; - if (!$GLOBALS['config']['HIDE_TIMESTAMPS'] || isLoggedIn()) echo ''.escape($rfc822date)."\n"; - if ($link['tags']!='') // Adding tags to each RSS entry (as mentioned in RSS specification) - { - foreach(explode(' ',$link['tags']) as $tag) { echo ''.$tag.''."\n"; } - } - - // Add permalink in description - $descriptionlink = '(Permalink)'; - // If user wants permalinks first, put the final link in description - if ($usepermalinks===true) $descriptionlink = '(Link)'; - if (strlen($link['description'])>0) $descriptionlink = '
'.$descriptionlink; - echo '' . "\n
\n"; - $i++; - } - echo '
'; - - $cache->cache(ob_get_contents()); - ob_end_flush(); - exit; -} - -// ------------------------------------------------------------------------------------------ -// Output the last N links in ATOM format. -function showATOM() -{ - header('Content-Type: application/atom+xml; charset=utf-8'); - - // $usepermalink : If true, use permalink instead of final link. - // User just has to add 'permalink' in URL parameters. e.g. http://mysite.com/shaarli/?do=atom&permalinks - $usepermalinks = isset($_GET['permalinks']) || !$GLOBALS['config']['ENABLE_RSS_PERMALINKS']; - - // Cache system - $query = $_SERVER["QUERY_STRING"]; - $cache = new CachedPage( - $GLOBALS['config']['PAGECACHE'], - page_url($_SERVER), - startsWith($query,'do=atom') && !isLoggedIn() - ); - $cached = $cache->cachedVersion(); - if (!empty($cached)) { - echo $cached; - exit; - } - - // If cached was not found (or not usable), then read the database and build the response: - // Read links from database (and filter private links if used it not logged in). - $LINKSDB = new LinkDB( - $GLOBALS['config']['DATASTORE'], - isLoggedIn(), - $GLOBALS['config']['HIDE_PUBLIC_LINKS'], - $GLOBALS['redirector'] - ); - - // Optionally filter the results: - if (!empty($_GET['searchterm'])) { - $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']); - } - else if (!empty($_GET['searchtags'])) { - $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags'])); - } - else { - $linksToDisplay = $LINKSDB; - } - - $nblinksToDisplay = 50; // Number of links to display. - // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links. - if (!empty($_GET['nb'])) { - $nblinksToDisplay = $_GET['nb']=='all' ? count($linksToDisplay) : max(intval($_GET['nb']), 1); - } - - $pageaddr=escape(index_url($_SERVER)); - $latestDate = ''; - $entries=''; - $i=0; - $keys=array(); foreach($linksToDisplay as $key=>$value) { $keys[]=$key; } // No, I can't use array_keys(). - while ($i<$nblinksToDisplay && $i'; - if ($usepermalinks===true) - $entries.=''.$guid.''; - else - $entries.=''.$guid.''; - if (!$GLOBALS['config']['HIDE_TIMESTAMPS'] || isLoggedIn()) $entries.=''.escape($iso8601date).''; - - // Add permalink in description - $descriptionlink = '(Permalink)'; - // If user wants permalinks first, put the final link in description - if ($usepermalinks===true) $descriptionlink = '(Link)'; - if (strlen($link['description'])>0) $descriptionlink = '
'.$descriptionlink; - - $entries .= '\n"; - if ($link['tags']!='') // Adding tags to each ATOM entry (as mentioned in ATOM specification) - { - foreach(explode(' ',$link['tags']) as $tag) - { $entries.=''."\n"; } - } - $entries.="\n"; - $i++; - } - $feed=''; - $feed.=''.$GLOBALS['title'].''; - if (!$GLOBALS['config']['HIDE_TIMESTAMPS'] || isLoggedIn()) $feed.=''.escape($latestDate).''; - $feed.=''; - if (!empty($GLOBALS['config']['PUBSUBHUB_URL'])) - { - $feed.=''; - $feed.=''; - $feed.=''; - } - $feed.=''.$pageaddr.''.$pageaddr.''; - $feed.=''.$pageaddr.''."\n\n"; // Yes, I know I should use a real IRI (RFC3987), but the site URL will do. - $feed.=$entries; - $feed.=''; - echo $feed; - - $cache->cache(ob_get_contents()); - ob_end_flush(); - exit; -} - // ------------------------------------------------------------------------------------------ // Daily RSS feed: 1 RSS entry per day giving all the links on that day. // Gives the last 7 days (which have links). // This RSS feed cannot be filtered. function showDailyRSS() { // Cache system - $query = $_SERVER["QUERY_STRING"]; + $query = $_SERVER['QUERY_STRING']; $cache = new CachedPage( $GLOBALS['config']['PAGECACHE'], page_url($_SERVER), @@ -919,7 +587,8 @@ function showDailyRSS() { $GLOBALS['config']['DATASTORE'], isLoggedIn(), $GLOBALS['config']['HIDE_PUBLIC_LINKS'], - $GLOBALS['redirector'] + $GLOBALS['redirector'], + $GLOBALS['config']['REDIRECTOR_URLENCODE'] ); /* Some Shaarlies may have very few links, so we need to look @@ -961,8 +630,7 @@ function showDailyRSS() { // For each day. foreach ($days as $day => $linkdates) { - $daydate = linkdate2timestamp($day.'_000000'); // Full text date - $rfc822date = linkdate2rfc822($day.'_000000'); + $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. @@ -975,7 +643,8 @@ function showDailyRSS() { $l = $LINKSDB[$linkdate]; $l['formatedDescription'] = format_description($l['description'], $GLOBALS['redirector']); $l['thumbnail'] = thumbnail($l['url']); - $l['timestamp'] = linkdate2timestamp($l['linkdate']); + $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 } @@ -985,10 +654,10 @@ function showDailyRSS() { // Then build the HTML for this day: $tpl = new RainTPL; $tpl->assign('title', $GLOBALS['title']); - $tpl->assign('daydate', $daydate); + $tpl->assign('daydate', $dayDate->getTimestamp()); $tpl->assign('absurl', $absurl); $tpl->assign('links', $links); - $tpl->assign('rfc822date', escape($rfc822date)); + $tpl->assign('rssdate', escape($dayDate->format(DateTime::RSS))); $html = $tpl->draw('dailyrss', $return_string=true); echo $html . PHP_EOL; @@ -1004,16 +673,10 @@ function showDailyRSS() { * Show the 'Daily' page. * * @param PageBuilder $pageBuilder Template engine wrapper. + * @param LinkDB $LINKSDB LinkDB instance. */ -function showDaily($pageBuilder) +function showDaily($pageBuilder, $LINKSDB) { - $LINKSDB = new LinkDB( - $GLOBALS['config']['DATASTORE'], - isLoggedIn(), - $GLOBALS['config']['HIDE_PUBLIC_LINKS'], - $GLOBALS['redirector'] - ); - $day=Date('Ymd',strtotime('-1 day')); // Yesterday, in format YYYYMMDD. if (isset($_GET['day'])) $day=$_GET['day']; @@ -1029,7 +692,7 @@ function showDaily($pageBuilder) } try { - $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_DAY, $day); + $linksToDisplay = $LINKSDB->filterDay($day); } catch (Exception $exc) { error_log($exc); $linksToDisplay = array(); @@ -1044,7 +707,8 @@ function showDaily($pageBuilder) $linksToDisplay[$key]['taglist']=$taglist; $linksToDisplay[$key]['formatedDescription'] = format_description($link['description'], $GLOBALS['redirector']); $linksToDisplay[$key]['thumbnail'] = thumbnail($link['url']); - $linksToDisplay[$key]['timestamp'] = linkdate2timestamp($link['linkdate']); + $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']); + $linksToDisplay[$key]['timestamp'] = $date->getTimestamp(); } /* We need to spread the articles on 3 columns. @@ -1069,11 +733,11 @@ function showDaily($pageBuilder) $fill[$index]+=$length; } + $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000'); $data = array( 'linksToDisplay' => $linksToDisplay, - 'linkcount' => count($LINKSDB), 'cols' => $columns, - 'day' => linkdate2timestamp($day.'_000000'), + 'day' => $dayDate->getTimestamp(), 'previousday' => $previousday, 'nextday' => $nextday, ); @@ -1103,10 +767,32 @@ function renderPage() $GLOBALS['config']['DATASTORE'], isLoggedIn(), $GLOBALS['config']['HIDE_PUBLIC_LINKS'], - $GLOBALS['redirector'] + $GLOBALS['redirector'], + $GLOBALS['config']['REDIRECTOR_URLENCODE'] + ); + + $updater = new Updater( + read_updates_file($GLOBALS['config']['UPDATES_FILE']), + $GLOBALS, + $LINKSDB, + isLoggedIn() ); + try { + $newUpdates = $updater->update(); + if (! empty($newUpdates)) { + write_updates_file( + $GLOBALS['config']['UPDATES_FILE'], + $updater->getDoneUpdates() + ); + } + } + catch(Exception $e) { + die($e->getMessage()); + } - $PAGE = new pageBuilder; + $PAGE = new PageBuilder(); + $PAGE->assign('linkcount', count($LINKSDB)); + $PAGE->assign('privateLinkcount', count_private($LINKSDB)); // Determine which page will be rendered. $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : ''; @@ -1115,9 +801,9 @@ function renderPage() // Call plugin hooks for header, footer and includes, specifying which page will be rendered. // Then assign generated data to RainTPL. $common_hooks = array( + 'includes', 'header', 'footer', - 'includes', ); $pluginManager = PluginManager::getInstance(); foreach($common_hooks as $name) { @@ -1137,12 +823,15 @@ function renderPage() if ($GLOBALS['config']['OPEN_SHAARLI']) { header('Location: ?'); exit; } // No need to login for open Shaarli $token=''; if (ban_canLogin()) $token=getToken(); // 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']):'')); $PAGE->renderPage('loginform'); exit; } // -------- User wants to logout. - if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=logout')) + if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=logout')) { invalidateCaches($GLOBALS['config']['PAGECACHE']); logout(); @@ -1154,16 +843,7 @@ function renderPage() if ($targetPage == Router::$PAGE_PICWALL) { // Optionally filter the results: - if (!empty($_GET['searchterm'])) { - $links = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']); - } - elseif (! empty($_GET['searchtags'])) { - $links = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags'])); - } - else { - $links = $LINKSDB; - } - + $links = $LINKSDB->filterSearch($_GET); $linksToDisplay = array(); // Get only links which have a thumbnail. @@ -1179,7 +859,6 @@ function renderPage() } $data = array( - 'linkcount' => count($LINKSDB), 'linksToDisplay' => $linksToDisplay, ); $pluginManager->executeHooks('render_picwall', $data, array('loggedin' => isLoggedIn())); @@ -1199,17 +878,36 @@ function renderPage() // We sort tags alphabetically, then choose a font size according to count. // First, find max value. - $maxcount=0; foreach($tags as $key=>$value) $maxcount=max($maxcount,$value); - ksort($tags); - $tagList=array(); - foreach($tags as $key=>$value) - // Tag font size scaling: default 15 and 30 logarithm bases affect scaling, 22 and 6 are arbitrary font sizes for max and min sizes. - { - $tagList[$key] = array('count'=>$value,'size'=>log($value, 15) / log($maxcount, 30) * (22-6) + 6); + $maxcount = 0; + foreach ($tags as $value) { + $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); + }); + + $tagList = array(); + foreach($tags as $key => $value) { + // Tag font size scaling: + // default 15 and 30 logarithm bases affect scaling, + // 22 and 6 are arbitrary font sizes for max and min sizes. + $size = log($value, 15) / log($maxcount, 30) * 2.2 + 0.8; + $tagList[$key] = array( + 'count' => $value, + 'size' => number_format($size, 2, '.', ''), + ); } $data = array( - 'linkcount' => count($LINKSDB), 'tags' => $tagList, ); $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn())); @@ -1224,7 +922,50 @@ function renderPage() // Daily page. if ($targetPage == Router::$PAGE_DAILY) { - showDaily($PAGE); + showDaily($PAGE, $LINKSDB); + } + + // ATOM and RSS feed. + if ($targetPage == Router::$PAGE_FEED_ATOM || $targetPage == Router::$PAGE_FEED_RSS) { + $feedType = $targetPage == Router::$PAGE_FEED_RSS ? FeedBuilder::$FEED_RSS : FeedBuilder::$FEED_ATOM; + header('Content-Type: application/'. $feedType .'+xml; charset=utf-8'); + + // Cache system + $query = $_SERVER['QUERY_STRING']; + $cache = new CachedPage( + $GLOBALS['config']['PAGECACHE'], + page_url($_SERVER), + startsWith($query,'do='. $targetPage) && !isLoggedIn() + ); + $cached = $cache->cachedVersion(); + if (!empty($cached)) { + echo $cached; + exit; + } + + // Generate data. + $feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, isLoggedIn()); + $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0))); + $feedGenerator->setHideDates($GLOBALS['config']['HIDE_TIMESTAMPS'] && !isLoggedIn()); + $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$GLOBALS['config']['ENABLE_RSS_PERMALINKS']); + if (!empty($GLOBALS['config']['PUBSUBHUB_URL'])) { + $feedGenerator->setPubsubhubUrl($GLOBALS['config']['PUBSUBHUB_URL']); + } + $data = $feedGenerator->buildData(); + + // Process plugin hook. + $pluginManager = PluginManager::getInstance(); + $pluginManager->executeHooks('render_feed', $data, array( + 'loggedin' => isLoggedIn(), + 'target' => $targetPage, + )); + + // Render the template. + $PAGE->assignAll($data); + $PAGE->renderPage('feed.'. $feedType); + $cache->cache(ob_get_contents()); + ob_end_flush(); + exit; } // Display openseach plugin (XML) @@ -1339,12 +1080,6 @@ function renderPage() exit; } - // Same case as above except that user tried to access ?do=addlink without being logged in - // Note: passing empty parameters makes Shaarli generate default URLs and descriptions. - if (isset($_GET['do']) && $_GET['do'] === 'addlink') { - header('Location: ?do=login&post='); - exit; - } showLinkList($PAGE, $LINKSDB); if (isset($_GET['edit_link'])) { header('Location: ?do=login&edit_link='. escape($_GET['edit_link'])); @@ -1360,7 +1095,6 @@ function renderPage() if ($targetPage == Router::$PAGE_TOOLS) { $data = array( - 'linkcount' => count($LINKSDB), 'pageabsaddr' => index_url($_SERVER), ); $pluginManager->executeHooks('render_tools', $data); @@ -1405,7 +1139,6 @@ function renderPage() } else // show the change password form. { - $PAGE->assign('linkcount',count($LINKSDB)); $PAGE->assign('token',getToken()); $PAGE->renderPage('changepassword'); exit; @@ -1417,11 +1150,15 @@ function renderPage() { if (!empty($_POST['title']) ) { - if (!tokenOk($_POST['token'])) die('Wrong token.'); // Go away! + if (!tokenOk($_POST['token'])) { + die('Wrong token.'); // Go away! + } $tz = 'UTC'; - if (!empty($_POST['continent']) && !empty($_POST['city'])) - if (isTimeZoneValid($_POST['continent'],$_POST['city'])) - $tz = $_POST['continent'].'/'.$_POST['city']; + if (!empty($_POST['continent']) && !empty($_POST['city']) + && isTimeZoneValid($_POST['continent'], $_POST['city']) + ) { + $tz = $_POST['continent'] . '/' . $_POST['city']; + } $GLOBALS['timezone'] = $tz; $GLOBALS['title']=$_POST['title']; $GLOBALS['titleLink']=$_POST['titleLink']; @@ -1449,7 +1186,6 @@ function renderPage() } else // Show the configuration form. { - $PAGE->assign('linkcount',count($LINKSDB)); $PAGE->assign('token',getToken()); $PAGE->assign('title', empty($GLOBALS['title']) ? '' : $GLOBALS['title'] ); $PAGE->assign('redirector', empty($GLOBALS['redirector']) ? '' : $GLOBALS['redirector'] ); @@ -1465,7 +1201,6 @@ function renderPage() if ($targetPage == Router::$PAGE_CHANGETAG) { if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) { - $PAGE->assign('linkcount', count($LINKSDB)); $PAGE->assign('token', getToken()); $PAGE->assign('tags', $LINKSDB->allTags()); $PAGE->renderPage('changetag'); @@ -1478,9 +1213,9 @@ function renderPage() // Delete a tag: if (isset($_POST['deletetag']) && !empty($_POST['fromtag'])) { - $needle=trim($_POST['fromtag']); + $needle = trim($_POST['fromtag']); // True for case-sensitive tag search. - $linksToAlter = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $needle, true); + $linksToAlter = $LINKSDB->filterSearch(array('searchtags' => $needle), true); foreach($linksToAlter as $key=>$value) { $tags = explode(' ',trim($value['tags'])); @@ -1495,9 +1230,9 @@ function renderPage() // Rename a tag: if (isset($_POST['renametag']) && !empty($_POST['fromtag']) && !empty($_POST['totag'])) { - $needle=trim($_POST['fromtag']); + $needle = trim($_POST['fromtag']); // True for case-sensitive tag search. - $linksToAlter = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $needle, true); + $linksToAlter = $LINKSDB->filterSearch(array('searchtags' => $needle), true); foreach($linksToAlter as $key=>$value) { $tags = explode(' ',trim($value['tags'])); @@ -1514,7 +1249,6 @@ function renderPage() // -------- User wants to add a link without using the bookmarklet: Show form. if ($targetPage == Router::$PAGE_ADDLINK) { - $PAGE->assign('linkcount',count($LINKSDB)); $PAGE->renderPage('addlink'); exit; } @@ -1522,21 +1256,42 @@ function renderPage() // -------- User clicked the "Save" button when editing a link: Save link to database. if (isset($_POST['save_edit'])) { - if (!tokenOk($_POST['token'])) die('Wrong token.'); // Go away! - $tags = trim(preg_replace('/\s\s+/',' ', $_POST['lf_tags'])); // Remove multiple spaces. - $tags = implode(' ', array_unique(explode(' ', $tags))); // Remove duplicates. - $linkdate=$_POST['lf_linkdate']; + // Go away! + if (! tokenOk($_POST['token'])) { + die('Wrong token.'); + } + // Remove multiple spaces. + $tags = trim(preg_replace('/\s\s+/', ' ', $_POST['lf_tags'])); + // Remove first '-' char in tags. + $tags = preg_replace('/(^| )\-/', '$1', $tags); + // Remove duplicates. + $tags = implode(' ', array_unique(explode(' ', $tags))); + $linkdate = $_POST['lf_linkdate']; $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; - $link = array('title'=>trim($_POST['lf_title']),'url'=>$url,'description'=>trim($_POST['lf_description']),'private'=>(isset($_POST['lf_private']) ? 1 : 0), - 'linkdate'=>$linkdate,'tags'=>str_replace(',',' ',$tags)); - if ($link['title']=='') $link['title']=$link['url']; // If title is empty, use the URL as title. + if (! startsWith($url, 'http:') && ! startsWith($url, 'https:') + && ! startsWith($url, 'ftp:') && ! startsWith($url, 'magnet:') + && ! startsWith($url, '?') && ! startsWith($url, 'javascript:') + ) { + $url = 'http://' . $url; + } + + $link = array( + 'title' => trim($_POST['lf_title']), + 'url' => $url, + 'description' => $_POST['lf_description'], + 'private' => (isset($_POST['lf_private']) ? 1 : 0), + 'linkdate' => $linkdate, + 'tags' => str_replace(',', ' ', $tags) + ); + // If title is empty, use the URL as title. + if ($link['title'] == '') { + $link['title'] = $link['url']; + } $pluginManager->executeHooks('save_link', $link); $LINKSDB[$linkdate] = $link; - $LINKSDB->savedb($GLOBALS['config']['PAGECACHE']); // Save to disk. + $LINKSDB->savedb($GLOBALS['config']['PAGECACHE']); pubsubhub(); // If we are called from the bookmarklet, we must close the popup: @@ -1545,10 +1300,12 @@ function renderPage() exit; } - $returnurl = !empty($_POST['returnurl']) ? escape($_POST['returnurl']): '?'; + $returnurl = !empty($_POST['returnurl']) ? $_POST['returnurl'] : '?'; $location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link')); - $location .= '#'.smallHash($_POST['lf_linkdate']); // Scroll to the link which has been edited. - header('Location: '. $location); // After saving the link, redirect to the page the user was on. + // Scroll to the link which has been edited. + $location .= '#' . smallHash($_POST['lf_linkdate']); + // After saving the link, redirect to the page the user was on. + header('Location: '. $location); exit; } @@ -1617,7 +1374,6 @@ function renderPage() $link = $LINKSDB[$_GET['edit_link']]; // Read database if (!$link) { header('Location: ?'); exit; } // Link not found in database. $data = array( - 'linkcount' => count($LINKSDB), 'link' => $link, 'link_is_new' => false, 'token' => getToken(), @@ -1636,7 +1392,7 @@ function renderPage() // -------- User want to post a new link: Display link edit form. if (isset($_GET['post'])) { - $url = cleanup_url(escape($_GET['post'])); + $url = cleanup_url($_GET['post']); $link_is_new = false; // Check if URL is not already in database (in this case, we will edit the existing link) @@ -1661,8 +1417,8 @@ function renderPage() // Extract title. $title = html_extract_title($content); // Re-encode title in utf-8 if necessary. - if (! empty($title) && $charset != 'utf-8') { - $title = mb_convert_encoding($title, $charset, 'utf-8'); + if (! empty($title) && strtolower($charset) != 'utf-8') { + $title = mb_convert_encoding($title, 'utf-8', $charset); } } } @@ -1671,6 +1427,8 @@ function renderPage() $url = '?' . smallHash($linkdate); $title = 'Note: '; } + $url = escape($url); + $title = escape($title); $link = array( 'linkdate' => $linkdate, @@ -1683,7 +1441,6 @@ function renderPage() } $data = array( - 'linkcount' => count($LINKSDB), 'link' => $link, 'link_is_new' => $link_is_new, 'token' => getToken(), // XSRF protection. @@ -1701,48 +1458,52 @@ function renderPage() exit; } - // -------- Export as Netscape Bookmarks HTML file. - if ($targetPage == Router::$PAGE_EXPORT) - { - if (empty($_GET['what'])) - { - $PAGE->assign('linkcount',count($LINKSDB)); + if ($targetPage == Router::$PAGE_EXPORT) { + // Export links as a Netscape Bookmarks file + + if (empty($_GET['selection'])) { $PAGE->renderPage('export'); exit; } - $exportWhat=$_GET['what']; - if (!array_intersect(array('all','public','private'),array($exportWhat))) die('What are you trying to export???'); - header('Content-Type: text/html; charset=utf-8'); - header('Content-disposition: attachment; filename=bookmarks_'.$exportWhat.'_'.strval(date('Ymd_His')).'.html'); - $currentdate=date('Y/m/d H:i:s'); - echo << - - - -Bookmarks -

Bookmarks

-HTML; - foreach($LINKSDB as $link) - { - if ($exportWhat=='all' || - ($exportWhat=='private' && $link['private']!=0) || - ($exportWhat=='public' && $link['private']==0)) - { - echo '
'.$link['title']."\n"; - if ($link['description']!='') echo '
'.$link['description']."\n"; - } + // export as bookmarks_(all|private|public)_YYYYmmdd_HHMMSS.html + $selection = $_GET['selection']; + if (isset($_GET['prepend_note_url'])) { + $prependNoteUrl = $_GET['prepend_note_url']; + } else { + $prependNoteUrl = false; } - exit; + + try { + $PAGE->assign( + 'links', + NetscapeBookmarkUtils::filterAndFormat( + $LINKSDB, + $selection, + $prependNoteUrl, + index_url($_SERVER) + ) + ); + } catch (Exception $exc) { + header('Content-Type: text/plain; charset=utf-8'); + echo $exc->getMessage(); + exit; + } + $now = new DateTime(); + header('Content-Type: text/html; charset=utf-8'); + header( + 'Content-disposition: attachment; filename=bookmarks_' + .$selection.'_'.$now->format(LinkDB::LINK_DATE_FORMAT).'.html' + ); + $PAGE->assign('date', $now->format(DateTime::RFC822)); + $PAGE->assign('eol', PHP_EOL); + $PAGE->assign('selection', $selection); + $PAGE->renderPage('export.bookmarks'); + exit; } // -------- User is uploading a file for import - if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=upload')) + 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)) @@ -1752,20 +1513,67 @@ HTML; exit; } if (!tokenOk($_POST['token'])) die('Wrong token.'); - importFile(); + importFile($LINKSDB); exit; } // -------- Show upload/import dialog: if ($targetPage == Router::$PAGE_IMPORT) { - $PAGE->assign('linkcount',count($LINKSDB)); $PAGE->assign('token',getToken()); $PAGE->assign('maxfilesize',getMaxFileSize()); $PAGE->renderPage('import'); exit; } + // Plugin administration page + if ($targetPage == Router::$PAGE_PLUGINSADMIN) { + $pluginMeta = $pluginManager->getPluginsMeta(); + + // Split plugins into 2 arrays: ordered enabled plugins and disabled. + $enabledPlugins = array_filter($pluginMeta, function($v) { return $v['order'] !== false; }); + // Load parameters. + $enabledPlugins = load_plugin_parameter_values($enabledPlugins, $GLOBALS['plugins']); + uasort( + $enabledPlugins, + function($a, $b) { return $a['order'] - $b['order']; } + ); + $disabledPlugins = array_filter($pluginMeta, function($v) { return $v['order'] === false; }); + + $PAGE->assign('enabledPlugins', $enabledPlugins); + $PAGE->assign('disabledPlugins', $disabledPlugins); + $PAGE->renderPage('pluginsadmin'); + exit; + } + + // Plugin administration form action + if ($targetPage == Router::$PAGE_SAVE_PLUGINSADMIN) { + try { + if (isset($_POST['parameters_form'])) { + unset($_POST['parameters_form']); + foreach ($_POST as $param => $value) { + $GLOBALS['plugins'][$param] = escape($value); + } + } + else { + $GLOBALS['config']['ENABLED_PLUGINS'] = save_plugin_config($_POST); + } + writeConfig($GLOBALS, isLoggedIn()); + } + catch (Exception $e) { + error_log( + 'ERROR while saving plugin configuration:.' . PHP_EOL . + $e->getMessage() + ); + + // TODO: do not handle exceptions/errors in JS. + echo ''; + exit; + } + header('Location: ?do='. Router::$PAGE_PLUGINSADMIN); + exit; + } + // -------- Otherwise, simply display search form and links: showLinkList($PAGE, $LINKSDB); exit; @@ -1773,15 +1581,10 @@ HTML; // ----------------------------------------------------------------------------------------------- // Process the import file form. -function importFile() +function importFile($LINKSDB) { if (!isLoggedIn()) { die('Not allowed.'); } - $LINKSDB = new LinkDB( - $GLOBALS['config']['DATASTORE'], - isLoggedIn(), - $GLOBALS['config']['HIDE_PUBLIC_LINKS'], - $GLOBALS['redirector'] - ); + $filename=$_FILES['filetoupload']['name']; $filesize=$_FILES['filetoupload']['size']; $data=file_get_contents($_FILES['filetoupload']['tmp_name']); @@ -1802,7 +1605,7 @@ function importFile() { $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 @@ -1861,46 +1664,32 @@ function importFile() } } -// ----------------------------------------------------------------------------------------------- -// Template for the list of links (