From: Seb Sauvage Date: Fri, 16 Sep 2011 09:26:26 +0000 (+0200) Subject: Version 0.0.7 beta X-Git-Url: https://git.immae.eu/?p=github%2Fshaarli%2FShaarli.git;a=commitdiff_plain;h=ef734b5dd7b21678313283e8604e5eaacfef0b10 Version 0.0.7 beta --- ef734b5dd7b21678313283e8604e5eaacfef0b10 diff --git a/index.php b/index.php new file mode 100644 index 00000000..3b6869f0 --- /dev/null +++ b/index.php @@ -0,0 +1,1102 @@ +'); // Suffix to encapsulate data in php code. +autoLocale(); // Sniff browser language and set date format accordingly. +header('Content-Type: text/html; charset=utf-8'); // We use UTF-8 for proper international characters handling. +$LINKSDB=false; + +// ----------------------------------------------------------------------------------------------- +// Log to text file +function logm($message) +{ + if (!file_exists(DATADIR.'/log.txt')) {$logFile = fopen(DATADIR.'/log.txt','w'); } + else { $logFile = fopen(DATADIR.'/log.txt','a'); } + fwrite($logFile,strval(date('Y/m/d_H:i:s')).' - '.$_SERVER["REMOTE_ADDR"].' - '.strval($message)."\n"); + fclose($logFile); +} + +// ------------------------------------------------------------------------------------------ +// 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() +{ + $loc='en_US'; // Default if browser does not send HTTP_ACCEPT_LANGUAGE + if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) // eg. "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. Prefered language is always presented first.) + if (preg_match('/([a-z]{2}(-[a-z]{2})?)/i',$_SERVER['HTTP_ACCEPT_LANGUAGE'],$matches)) $loc=$matches[1]; + } + setlocale(LC_TIME,$loc); // LC_TIME = Set local for date/time format only. +} + +// ------------------------------------------------------------------------------------------ +// Session management +define('INACTIVITY_TIMEOUT',3600); // (in seconds). If the user does not access any page within this time, his/her session is considered expired. +ini_set('session.use_cookies', 1); // Use cookies to store session. +ini_set('session.use_only_cookies', 1); // Force cookies for session (phpsessionID forbidden in URL) +ini_set('session.use_trans_sid', false); // Prevent php to use sessionID in URL if cookies are disabled. +session_name('shaarli'); +session_start(); + +// Returns the IP address of the client (Used to prevent session cookie hijacking.) +function allIPs() +{ + $ip = $_SERVER["REMOTE_ADDR"]; + // Then we use more HTTP headers to prevent session hijacking from users behind the same proxy. + if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $ip=$ip.'_'.$_SERVER['HTTP_X_FORWARDED_FOR']; } + if (isset($_SERVER['HTTP_CLIENT_IP'])) { $ip=$ip.'_'.$_SERVER['HTTP_CLIENT_IP']; } + return $ip; +} + +// Check that user/password is correct. +function check_auth($login,$password) +{ + $hash = sha1($password.$login.$GLOBALS['salt']); + if ($login==$GLOBALS['login'] && $hash==$GLOBALS['hash']) + { // Login/password is correct. + $_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']=$login; + $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // Set session expiration. + logm('Login successful'); + return True; + } + logm('Login failed for user '.$login); + return False; +} + +// Returns true if the user is logged in. +function isLoggedIn() +{ + // If session does not exist on server side, or IP address has changed, or session has expired, logout. + if (empty($_SESSION['uid']) || $_SESSION['ip']!=allIPs() || time()>=$_SESSION['expires_on']) + { + logout(); + return false; + } + $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT; // User accessed a page : Update his/her session expiration date. + return true; +} + +// Force logout. +function logout() { unset($_SESSION['uid']); unset($_SESSION['ip']); unset($_SESSION['username']);} + + +// ------------------------------------------------------------------------------------------ +// Brute force protection system +// Several consecutive failed logins will ban the IP address for 30 minutes. +if (!is_file(IPBANS_FILENAME)) file_put_contents(IPBANS_FILENAME, "array(),'BANS'=>array()),true).";\n?>"); +include IPBANS_FILENAME; +// Signal a failed login. Will ban the IP if too many failures: +function ban_loginFailed() +{ + $ip=$_SERVER["REMOTE_ADDR"]; $gb=$GLOBALS['IPBANS']; + if (!isset($gb['FAILURES'][$ip])) $gb['FAILURES'][$ip]=0; + $gb['FAILURES'][$ip]++; + if ($gb['FAILURES'][$ip]>(BAN_AFTER-1)) + { + $gb['BANS'][$ip]=time()+BAN_DURATION; + logm('IP address banned from login'); + } + $GLOBALS['IPBANS'] = $gb; + file_put_contents(IPBANS_FILENAME, ""); +} + +// Signals a successful login. Resets failed login counter. +function ban_loginOk() +{ + $ip=$_SERVER["REMOTE_ADDR"]; $gb=$GLOBALS['IPBANS']; + unset($gb['FAILURES'][$ip]); unset($gb['BANS'][$ip]); + $GLOBALS['IPBANS'] = $gb; + file_put_contents(IPBANS_FILENAME, ""); +} + +// Checks if the user CAN login. If 'true', the user can try to login. +function ban_canLogin() +{ + $ip=$_SERVER["REMOTE_ADDR"]; $gb=$GLOBALS['IPBANS']; + if (isset($gb['BANS'][$ip])) + { + // 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.'); + unset($gb['FAILURES'][$ip]); unset($gb['BANS'][$ip]); + file_put_contents(IPBANS_FILENAME, ""); + return true; // Ban has expired, user can login. + } + return false; // User is banned. + } + return true; // User is not banned. +} + +// ------------------------------------------------------------------------------------------ +// Process login form: Check if login/password is correct. +if (isset($_POST['login'])) +{ + if (!ban_canLogin()) die('I said: NO. You are banned for the moment. Go away.'); + if (isset($_POST['password']) && tokenOk($_POST['token']) && (check_auth($_POST['login'], $_POST['password']))) + { // Login/password is ok. + ban_loginOk(); + // Optional redirect after login: + if (isset($_GET['post'])) { header('Location: ?post='.urlencode($_GET['post']).(!empty($_GET['source'])?'&source='.urlencode($_GET['source']):'')); exit; } + if (isset($_POST['returnurl'])) { header('Location: '.$_POST['returnurl']); exit; } + header('Location: ?'); exit; + } + else + { + ban_loginFailed(); + echo ''; // Redirect to login screen. + exit; + } +} + +// ------------------------------------------------------------------------------------------ +// Misc utility functions: + +// Returns the server URL (including port and http/https), without path. +// eg. "http://myserver.com:8080" +// You can append $_SERVER['SCRIPT_NAME'] to get the current script URL. +function serverUrl() +{ + $serverport = ($_SERVER["SERVER_PORT"]!='80' ? ':'.$_SERVER["SERVER_PORT"] : ''); + return 'http'.(!empty($_SERVER['HTTPS'])?'s':'').'://'.$_SERVER["SERVER_NAME"].$serverport; +} + +// Convert post_max_size/upload_max_filesize (eg.'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 ? (eg. 2M instead of 2000000) + return $maxsize; +} + +// Tells if a string start with a substring or not. +function startsWith($haystack,$needle,$case=true) +{ + if($case){return (strcmp(substr($haystack, 0, strlen($needle)),$needle)===0);} + return (strcasecmp(substr($haystack, 0, strlen($needle)),$needle)===0); +} + +// Tells if a string ends with a substring or not. +function endsWith($haystack,$needle,$case=true) +{ + if($case){return (strcmp(substr($haystack, strlen($haystack) - strlen($needle)),$needle)===0);} + return (strcasecmp(substr($haystack, strlen($haystack) - strlen($needle)),$needle)===0); +} + +/* 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) +{ + $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); +} + +/* 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 localized date format. + (used to display link date on screen) + The date format is automatically chose according to locale/languages sniffed from browser headers (see autoLocale()). */ +function linkdate2locale($linkdate) +{ + return utf8_encode(strftime('%c',linkdate2timestamp($linkdate))); // %c is for automatic date format according to locale. + // Note that if you use a local which is not installed on your webserver, + // the date will not be displayed in the chosen locale, but probably in US notation. +} + +// Parse HTTP response headers and return an associative array. +function http_parse_headers( $headers ) +{ + $res=array(); + foreach($headers as $header) + { + $i = strpos($header,': '); + if ($i) + { + $key=substr($header,0,$i); + $value=substr($header,$i+2,strlen($header)-$i-2); + $res[$key]=$value; + } + } + return $res; +} + +/* GET an URL. + Input: $url : url to get (http://...) + $timeout : Network timeout (will wait this many seconds for an anwser before giving up). + Output: An array. [0] = HTTP status message (eg. "HTTP/1.1 200 OK") + [1] = associative array containing HTTP response headers (eg. echo getHTTP($url)[1]['Content-Type']) + [2] = data + Example: list($httpstatus,$headers,$data) = getHTTP('http://sebauvage.net/'); + if (strpos($httpstatus,'200 OK')) + echo 'Data type: '.htmlspecialchars($headers['Content-Type']); + else + echo 'There was an error: '.htmlspecialchars($httpstatus) +*/ +function getHTTP($url,$timeout=30) +{ + //FIXME: trap error correctly (unresolved host, unsupported protocol, etc.) + $options = array('http'=>array('method'=>'GET','timeout' => $timeout)); // Force network timeout + $context = stream_context_create($options); + $data=file_get_contents($url,false,$context,-1, 2000000); // We download at most 2 Mb from source. + if (!$data) { $lasterror=error_get_last(); return array($lasterror['message'],array(),''); } + $httpStatus=$http_response_header[0]; // eg. "HTTP/1.1 200 OK" + $responseHeaders=http_parse_headers($http_response_header); + return array($httpStatus,$responseHeaders,$data); +} + +// Extract title from an HTML document. +// (Returns an empty string if not found.) +function html_extract_title($html) +{ + return preg_match('!(.*?)!i', $html, $matches) ? $matches[1] : ''; +} + +// ------------------------------------------------------------------------------------------ +// Token management for XSRF protection +// Token should be used in any form which acts on data (create,update,delete,import...). +if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are attached to the session. + +// Returns a token. +function getToken() +{ + $rnd = sha1(uniqid('',true).'_'.mt_rand()); // We generate a random string. + $_SESSION['tokens'][$rnd]=1; // Store it on the server side. + return $rnd; +} + +// Tells if a token is ok. Using this function will destroy the token. +// true=token is ok. +function tokenOk($token) +{ + if (isset($_SESSION['tokens'][$token])) + { + unset($_SESSION['tokens'][$token]); // Token is used: destroy it. + return true; // Token is ok. + } + return false; // Wrong token, or already used. +} + +// ------------------------------------------------------------------------------------------ +/* Data storage for links. + This object behaves like an associative array. + Example: + $mylinks = new linkdb(); + echo $mylinks['20110826_161819']['title']; + foreach($mylinks as $link) + echo $link['title'].' at url '.$link['url'].' ; description:'.$link['description']; + + We implement 3 interfaces: + - ArrayAccess so that this object behaves like an associative array. + - Iterator so that this object can be used in foreach() loops. + - Countable interface so that we can do a count() on this object. +*/ +class linkdb implements Iterator, Countable, ArrayAccess + +{ + private $links; // List of links (associative array. Key=linkdate (eg. "20110823_124546"), value= associative array (keys:title,description...) + private $urls; // List of all recorded URLs (key=url, value=linkdate) for fast reserve search (url-->linkdate) + private $keys; // List of linkdate keys (for the Iterator interface implementation) + private $position; // Position in the $this->keys array. (for the Iterator interface implementation.) + private $loggedin; // Is the used logged in ? (used to filter private links) + + // Constructor: + function __construct($isLoggedIn) + // Input : $isLoggedIn : is the used logged in ? + { + $this->loggedin = $isLoggedIn; + $this->checkdb(); // Make sure data file exists. + $this->readdb(); // Then read it. + } + + // ---- Countable interface implementation + public function count() { return count($this->links); } + + // ---- ArrayAccess interface implementation + public function offsetSet($offset, $value) + { + if (!$this->loggedin) die('You are not authorized to add a link.'); + if (empty($value['linkdate']) || empty($value['url'])) die('Internal Error: A link should always have a linkdate and url.'); + if (empty($offset)) die('You must specify a key.'); + $this->links[$offset] = $value; + $this->urls[$value['url']]=$offset; + } + public function offsetExists($offset) { return array_key_exists($offset,$this->links); } + public function offsetUnset($offset) + { + if (!$this->loggedin) die('You are not authorized to delete a link.'); + $url = $this->links[$offset]['url']; unset($this->urls[$url]); + unset($this->links[$offset]); + } + public function offsetGet($offset) { return isset($this->links[$offset]) ? $this->links[$offset] : null; } + + // ---- Iterator interface implementation + function rewind() { $this->keys=array_keys($this->links); rsort($this->keys); $this->position=0; } // Start over for iteration, ordered by date (latest first). + function key() { return $this->keys[$this->position]; } // current key + function current() { return $this->links[$this->keys[$this->position]]; } // current value + function next() { ++$this->position; } // go to next item + function valid() { return isset($this->keys[$this->position]); } // Check if current position is valid. + + // ---- Misc methods + private function checkdb() // Check if db directory and file exists. + { + if (!file_exists(DATASTORE)) // Create a dummy database for example. + { + $this->links = array(); + $link = array('title'=>'Shaarli - sebsauvage.net','url'=>'http://sebsauvage.net/wiki/doku.php?id=php:shaarli','description'=>'Welcome to Shaarli ! This is a bookmark. To edit or delete me, you must first login.','private'=>0,'linkdate'=>'20110914_190000','tags'=>'opensource software'); + $this->links[$link['linkdate']] = $link; + $link = array('title'=>'My secret stuff... - Pastebin.com','url'=>'http://pastebin.com/smCEEeSn','description'=>'SShhhh!! I\'m a private link only YOU can see. You can delete me too.','private'=>1,'linkdate'=>'20110914_074522','tags'=>'secretstuff'); + $this->links[$link['linkdate']] = $link; + file_put_contents(DATASTORE, PHPPREFIX.base64_encode(gzdeflate(serialize($this->links))).PHPSUFFIX); // Write database to disk + } + } + + // Read database from disk to memory + private function readdb() + { + // Read data + $this->links=(file_exists(DATASTORE) ? unserialize(gzinflate(base64_decode(substr(file_get_contents(DATASTORE),strlen(PHPPREFIX),-strlen(PHPSUFFIX))))) : array() ); + + // If user is not logged in, filter private links. + if (!$this->loggedin) + { + $toremove=array(); + foreach($this->links as $link) { if ($link['private']!=0) $toremove[]=$link['linkdate']; } + foreach($toremove as $linkdate) { unset($this->links[$linkdate]); } + } + + // Keep the list of the mapping URLs-->linkdate up-to-date. + $this->urls=array(); + foreach($this->links as $link) { $this->urls[$link['url']]=$link['linkdate']; } + } + + // Save database from memory to disk. + public function savedb() + { + if (!$this->loggedin) die('You are not authorized to change the database.'); + file_put_contents(DATASTORE, PHPPREFIX.base64_encode(gzdeflate(serialize($this->links))).PHPSUFFIX); + } + + // Returns the link for a given URL (if it exists). false it does not exist. + public function getLinkFromUrl($url) + { + if (isset($this->urls[$url])) return $this->links[$this->urls[$url]]; + return false; + } + + // Case insentitive search among links (in url, title and description). Returns filtered list of links. + // eg. print_r($mydb->filterTags('hollandais')); + public function filterFulltext($searchterms) + { + // FIXME: explode(' ',$searchterms) and perform a AND search. + // FIXME: accept double-quotes to search for a string "as is" ? + $filtered=array(); + $s = strtolower($searchterms); + foreach($this->links as $l) + { + $found=strpos(strtolower($l['title']),$s) || strpos(strtolower($l['description']),$s) || strpos(strtolower($l['url']),$s) || strpos(strtolower($l['tags']),$s); + if ($found) $filtered[$l['linkdate']] = $l; + } + krsort($filtered); + return $filtered; + } + + // Filter by tag. + // You can specify one or more tags (tags can be separated by space or comma). + // eg. print_r($mydb->filterTags('linux programming')); + public function filterTags($tags) + { + $t = str_replace(',',' ',strtolower($tags)); + $searchtags=explode(' ',$t); + $filtered=array(); + foreach($this->links as $l) + { + $linktags = explode(' ',strtolower($l['tags'])); + if (count(array_intersect($linktags,$searchtags)) == count($searchtags)) + $filtered[$l['linkdate']] = $l; + } + krsort($filtered); + return $filtered; + + } +} + +// ------------------------------------------------------------------------------------------ +// Ouput the last 50 links in RSS 2.0 format. +function showRSS() +{ + global $LINKSDB; + header('Content-Type: application/xhtml+xml; charset=utf-8'); + $pageaddr=htmlspecialchars(serverUrl().$_SERVER["SCRIPT_NAME"]); + echo ''; + echo 'Shared links on '.$pageaddr.''.$pageaddr.''; + echo 'Shared links'.$pageaddr.''."\n\n"; + $i=0; + $keys=array(); foreach($LINKSDB as $key=>$value) { $keys[]=$key; } // No, I can't use array_keys(). + while ($i<50 && $i'.htmlspecialchars($link['title']).''.$pageaddr.'?'.htmlspecialchars($link['linkdate']).''.htmlspecialchars($link['url']).''.htmlspecialchars($rfc822date).''; + echo ''."\n"; + $i++; + } + echo ''; + exit; +} + +// ------------------------------------------------------------------------------------------ +// Render HTML page: +function renderPage() +{ + global $STARTTIME; + global $LINKSDB; + + // Well... rendering the page would be 100x better with the excellent Smarty, but I don't want to tie this minimalist project to 100+ files. + // So I use a custom templating system. + + // -------- Display login form. + if (startswith($_SERVER["QUERY_STRING"],'do=login')) + { + if (!ban_canLogin()) + { + $loginform='
You have been banned from login after too many failed attempts. Try later.
'; + $data = array('pageheader'=>$loginform,'body'=>'','onload'=>''); + templatePage($data); + exit; + } + $returnurl_html = (isset($_SERVER['HTTP_REFERER']) ? '' : ''); + $loginform='
Login:    Password : '.$returnurl_html.'
'; + $onload = 'onload="document.loginform.login.focus();"'; + $data = array('pageheader'=>$loginform,'body'=>'','onload'=>$onload); + templatePage($data); + exit; + } + + // -------- User wants to logout. + if (startswith($_SERVER["QUERY_STRING"],'do=logout')) + { + logout(); + header('Location: ?'); + exit; + } + + // -------- User clicks on a tag in a link: The tag is added to the list of searched tags (searchtags=...) + if (isset($_GET['addtag'])) + { + // Get previous URL (http_referer) and add the tag to the searchtags parameters in query. + parse_str(parse_url($_SERVER['HTTP_REFERER'],PHP_URL_QUERY), $params); + $params['searchtags'] = (empty($params['searchtags']) ? trim($_GET['addtag']) : trim($params['searchtags'].' '.$_GET['addtag'])); + unset($params['page']); // We also remove page (keeping the same page has no sense, since the results are different) + header('Location: ?'.http_build_query($params)); + exit; + } + + // -------- User clicks on a tag in result count: Remove the tag from the list of searched tags (searchtags=...) + if (isset($_GET['removetag'])) + { + // Get previous URL (http_referer) and remove the tag from the searchtags parameters in query. + parse_str(parse_url($_SERVER['HTTP_REFERER'],PHP_URL_QUERY), $params); + if (isset($params['searchtags'])) + { + $tags = explode(' ',$params['searchtags']); + $tags=array_diff($tags, array($_GET['removetag'])); // Remove value from array $tags. + if (count($tags)==0) unset($params['searchtags']); else $params['searchtags'] = implode(' ',$tags); + unset($params['page']); // We also remove page (keeping the same page has no sense, since the results are different) + } + header('Location: ?'.http_build_query($params)); + exit; + } + + // -------- User wants to change the number of links per page (linksperpage=...) + if (isset($_GET['linksperpage'])) + { + if (is_numeric($_GET['linksperpage'])) { $_SESSION['LINKS_PER_PAGE']=abs(intval($_GET['linksperpage'])); } + header('Location: '.$_SERVER['HTTP_REFERER']); + exit; + } + + + // -------- Handle other actions allowed for non-logged in users: + if (!isLoggedIn()) + { + // User tries to post new link but is not loggedin: + // Show login screen, then redirect to ?post=... + if (isset($_GET['post'])) + { + header('Location: ?do=login&post='.urlencode($_GET['post']).(isset($_GET['source'])?'&source='.urlencode($_GET['source']):'')); // Redirect to login page, then back to post link. + exit; + } + + // Show search form and display list of links. + $searchform=<< +
+
+ +HTML; + $onload = 'document.searchform.searchterm.focus();'; + $data = array('pageheader'=>$searchform,'body'=>templateLinkList(),'onload'=>$onload); + templatePage($data); + exit; // Never remove this one ! + } + + // -------- All other functions are reserved for the registered user: + + // -------- Display the Tools menu if requested (import/export/bookmarklet...) + if (startswith($_SERVER["QUERY_STRING"],'do=tools')) + { + $pageabsaddr=serverUrl().$_SERVER["SCRIPT_NAME"]; // Why doesn't php have a built-in function for that ? + // The javascript code for the bookmarklet: + $toolbar= << + Import - Import Netscape html bookmarks (as exported from Firefox, Chrome, Opera, delicious...)
+ Export - Export Netscape html bookmarks (which can be imported in Firefox, Chrome, Opera, delicious...)
+ Shaare link - Drag this link to your bookmarks toolbar (or right-click it and choose Bookmark This Link....). Then click "Shaare link" button in any page you want to share. + +HTML; + $data = array('pageheader'=>$toolbar,'body'=>'','onload'=>''); + templatePage($data); + exit; + } + + // -------- User wants to add a link without using the bookmarklet: show form. + if (startswith($_SERVER["QUERY_STRING"],'do=addlink')) + { + $onload = 'document.addform.post.focus();'; + $addform= '
'; + $data = array('pageheader'=>$addform,'body'=>'','onload'=>$onload); + templatePage($data); + exit; + } + + // -------- 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 ! + $linkdate=$_POST['lf_linkdate']; + $link = array('title'=>trim($_POST['lf_title']),'url'=>trim($_POST['lf_url']),'description'=>trim($_POST['lf_description']),'private'=>(isset($_POST['lf_private']) ? 1 : 0), + 'linkdate'=>$linkdate,'tags'=>trim($_POST['lf_tags'])); + if ($link['title']=='') $link['title']=$link['url']; // If title is empty, use the URL as title. + $LINKSDB[$linkdate] = $link; + $LINKSDB->savedb(); // save to disk + + // If we are called from the bookmarklet, we must close the popup: + if (isset($_GET['source']) && $_GET['source']=='bookmarklet') { echo ''; exit; } + header('Location: '.$_POST['returnurl']); // After saving the link, redirect to the page the user was on. + exit; + } + + // -------- User clicked the "Cancel" button when editing a link. + if (isset($_POST['cancel_edit'])) + { + // If we are called from the bookmarklet, we must close the popup; + if (isset($_GET['source']) && $_GET['source']=='bookmarklet') { echo ''; exit; } + $returnurl = ( isset($_POST['returnurl']) ? $_POST['returnurl'] : '?' ); + 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 (!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']; + unset($LINKSDB[$linkdate]); + $LINKSDB->savedb(); // save to disk + // If we are called from the bookmarklet, we must close the popup: + if (isset($_GET['source']) && $_GET['source']=='bookmarklet') { echo ''; exit; } + $returnurl = ( isset($_POST['returnurl']) ? $_POST['returnurl'] : '?' ); + header('Location: '.$returnurl); // After deleting the link, redirect to the page the user was on. + exit; + } + + // -------- User clicked the "EDIT" button on a link: Display link edit form. + if (isset($_GET['edit_link'])) + { + $link = $LINKSDB[$_GET['edit_link']]; // Read database + if (!$link) { header('Location: ?'); exit; } // Link not found in database. + list($editform,$onload)=templateEditForm($link); + $data = array('pageheader'=>$editform,'body'=>'','onload'=>$onload); + templatePage($data); + exit; + } + + // -------- User want to post a new link: Display link edit form. + if (isset($_GET['post'])) + { + $url=$_GET['post']; + + // We remove the annoying parameters added by FeedBurner and GoogleFeedProxy (?utm_source=...) + $i=strpos($url,'&utm_source='); if ($i) $url=substr($url,0,$i); + $i=strpos($url,'?utm_source='); if ($i) $url=substr($url,0,$i); + + $link_is_new = false; + $link = $LINKSDB->getLinkFromUrl($url); // Check if URL is not already in database (in this case, we will edit the existing link) + if (!$link) + { + $link_is_new = true; // This is a new link + $linkdate = strval(date('Ymd_His')); + $title = (empty($_GET['title']) ? '' : $_GET['title'] ); // Get title if it was provided in URL (by the bookmarklet). + $description=''; $tags=''; $private=0; + if (parse_url($url,PHP_URL_SCHEME)=='') $url = 'http://'.$url; + // If this is an HTTP link, we try go get the page to extact the title (otherwise we will to straight to the edit form.) + if (empty($title) && parse_url($url,PHP_URL_SCHEME)=='http') + { + list($status,$headers,$data) = getHTTP($url,4); // Short timeout to keep the application responsive. + // FIXME: Decode charset according to charset specified in either 1) HTTP response headers or 2) in html + if (strpos($status,'200 OK')) $title=html_extract_title($data); + } + $link = array('linkdate'=>$linkdate,'title'=>$title,'url'=>$url,'description'=>$description,'tags'=>$tags,'private'=>0); + } + list($editform,$onload)=templateEditForm($link,$link_is_new); + $data = array('pageheader'=>$editform,'body'=>'','onload'=>$onload); + templatePage($data); + exit; + } + + // -------- Export as Netscape Bookmarks HTML file. + if (startswith($_SERVER["QUERY_STRING"],'do=export')) + { + header('Content-Type: text/html; charset=utf-8'); + header('Content-disposition: attachment; filename=bookmarks_'.strval(date('Ymd_His')).'.html'); + echo << + + +Bookmarks +

Bookmarks

+HTML; + foreach($LINKSDB as $link) + { + echo '
'.htmlspecialchars($link['title'])."\n"; + if ($link['description']!='') echo '
'.htmlspecialchars($link['description'])."\n"; + } + echo '\n"; + exit; + } + + // -------- User is uploading a file for import + if (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 ''; + exit; + } + if (!tokenOk($_POST['token'])) die('Wrong token.'); + importFile(); + exit; + } + + // -------- Show upload/import dialog: + if (startswith($_SERVER["QUERY_STRING"],'do=import')) + { + $token = getToken(); + $maxfilesize=getMaxFileSize(); + $onload = 'onload="document.uploadform.filetoupload.focus();"'; + $uploadform=<< +Import Netscape html bookmarks (as exported from Firefox/Chrome/Opera/delicious/diigo...) (Max: {$maxfilesize} bytes). + + + + + + + +HTML; + $data = array('pageheader'=>$uploadform,'body'=>'','onload'=>$onload ); + templatePage($data); + exit; + } + + // -------- Otherwise, simply display search form and links: + $searchform=<< +
+
+ +HTML; + $onload = 'document.searchform.searchterm.focus();'; + $data = array('pageheader'=>$searchform,'body'=>templateLinkList(),'onload'=>$onload); + templatePage($data); + exit; +} + +// ----------------------------------------------------------------------------------------------- +// Process the import file form. +function importFile() +{ + global $LINKSDB; + $filename=$_FILES['filetoupload']['name']; + $filesize=$_FILES['filetoupload']['size']; + $data=file_get_contents($_FILES['filetoupload']['tmp_name']); + + // 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. + // I didn't want to use DOM... anyway, this is FAST (less than 1 second to import 7200 links (2.1 Mb html file)). + $before=count($LINKSDB); + 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 + preg_match_all('! ([A-Z_]+)=\"(.*?)"!i',$html,$matches,PREG_SET_ORDER); // Get all other attributes + foreach($matches as $m) + { + $attr=$m[1]; $value=$m[2]; + if ($attr=='HREF') $link['url']=$value; + elseif ($attr=='ADD_DATE') $link['linkdate']=date('Ymd_His',intval($value)); + elseif ($attr=='PRIVATE') $link['private']=($value=='0'?0:1); + elseif ($attr=='TAGS') $link['tags']=str_replace(',',' ',$value); + } + if ($link['linkdate']!='' && $link['url']!='') $LINKSDB[$link['linkdate']] = $link; + } + } + $import_count = count($LINKSDB)-$before; + $LINKSDB->savedb(); + echo ''; + } + else + { + echo ''; + } +} + +// ----------------------------------------------------------------------------------------------- +/* Template for the edit link form + Input: $link : link to edit (assocative array item as returned by the LINKDB class) +Output: An array : (string) : The html code of the edit link form. + (string) : The proper onload to use in body. + Example: list($html,$onload)=templateEditForm($mylinkdb['20110805_124532']); + echo $html; +*/ +function templateEditForm($link,$link_is_new=false) +{ + $url=htmlspecialchars($link['url']); + $title=htmlspecialchars($link['title']); + $tags=htmlspecialchars($link['tags']); + $description=htmlspecialchars($link['description']); + $private = ($link['private']==0 ? '' : 'checked="yes"'); + + // Automatically focus on empty fields: + $onload='onload="document.linkform.lf_tags.focus();"'; + if ($description=='') $onload='onload="document.linkform.lf_description.focus();"'; + if ($title=='') $onload='onload="document.linkform.lf_title.focus();"'; + + // Do not show "Delete" button if this is a new link. + $delete_button = ''; + if ($link_is_new) $delete_button=''; + + $token=getToken(); // XSRF protection. + $returnurl_html = (isset($_SERVER['HTTP_REFERER']) ? '' : ''); + $editlinkform=<< +
+ + URL

+ Title

+ Description

+ Tags

+  Private
+ + + {$delete_button} + + {$returnurl_html} +
+ +HTML; + return array($editlinkform,$onload); +} + + +// ----------------------------------------------------------------------------------------------- +// Template for the list of links. +// Returns html code to show the list of link according to parameters passed in URL (search terms, page...) +function templateLinkList() +{ + global $LINKSDB; + + // Search according to entered search terms: + $linksToDisplay=array(); + $searched=''; + if (!empty($_GET['searchterm'])) // Fulltext search + { + $linksToDisplay = $LINKSDB->filterFulltext($_GET['searchterm']); + $searched=' '.count($linksToDisplay).' results for '.htmlspecialchars($_GET['searchterm']).':'; + } + elseif (!empty($_GET['searchtags'])) // Search by tag + { + $linksToDisplay = $LINKSDB->filterTags($_GET['searchtags']); + $tagshtml=''; foreach(explode(' ',$_GET['searchtags']) as $tag) $tagshtml.=''.htmlspecialchars($tag).' x '; + $searched=' '.count($linksToDisplay).' results for tags '.$tagshtml.':'; + } + else + $linksToDisplay = $LINKSDB; // otherwise, display without filtering. + + $linklist=''; + $actions=''; + + // Handle paging. + /* Can someone explain to me why you get the following error when using array_keys() on an object which implements the interface ArrayAccess ??? + "Warning: array_keys() expects parameter 1 to be array, object given in ... " + If my class implements ArrayAccess, why won't array_keys() accept it ? ( $keys=array_keys($linksToDisplay); ) + */ + $keys=array(); foreach($linksToDisplay as $key=>$value) { $keys[]=$key; } // Stupid and ugly. Thanks php. + $pagecount = ceil(count($keys)/$_SESSION['LINKS_PER_PAGE']); + $pagecount = ($pagecount==0 ? 1 : $pagecount); + $page=( empty($_GET['page']) ? 1 : intval($_GET['page'])); + $page = ( $page<1 ? 1 : $page ); + $page = ( $page>$pagecount ? $pagecount : $page ); + $i = ($page-1)*$_SESSION['LINKS_PER_PAGE']; // Start index. + $end = $i+$_SESSION['LINKS_PER_PAGE']; + while ($i<$end && $i'; + $tags=''; + if ($link['tags']!='') foreach(explode(' ',$link['tags']) as $tag) { $tags.=''.htmlspecialchars($tag).' '; } + $linklist.='
  • '.htmlspecialchars($title).''.$actions.'
    '; + if ($description!='') $linklist.='
    '.str_replace("\n",'
    ',htmlspecialchars($description)).'

    '; + $linklist.=''.htmlspecialchars(linkdate2locale($link['linkdate'])).' - '.htmlspecialchars($link['url']).'
    '.$tags."
  • \n"; + $i++; + } + + // Show paging. + $searchterm= ( empty($_GET['searchterm']) ? '' : '&searchterm='.$_GET['searchterm'] ); + $searchtags= ( empty($_GET['searchtags']) ? '' : '&searchtags='.$_GET['searchtags'] ); + $paging=''; + if ($i!=count($keys)) $paging.='◄Older'; + $paging.= 'page '.$page.' / '.$pagecount.''; + if ($page>1) $paging.='Newer►'; + $linksperpage = << +Links per page: 20 50 100 +
    +HTML; + $paging = '
    '.$linksperpage.$paging.'
    '; + $linklist=''; + return $linklist; +} + +// ----------------------------------------------------------------------------------------------- +// Template for the whole page. +/* Input: $data (associative array). + Keys: 'body' : body of HTML document + 'pageheader' : html code to show in page header (top of page) + 'onload' : optional onload javascript for the +*/ +function templatePage($data) +{ + global $STARTTIME; + global $LINKSDB; + $shaarli_version = shaarli_version; + $linkcount = count($LINKSDB); + $menu=(isLoggedIn() ? ' Logout  Tools  Add link' : ' Login'); + foreach(array('pageheader','body','onload') as $k) // make sure all required fields exist (put an empty string if not). + { + if (!array_key_exists($k,$data)) $data[$k]=''; + } + $feedurl=htmlspecialchars(serverUrl().$_SERVER['SCRIPT_NAME'].'?do=rss'); + echo << + +Shaarli - Let's shaare your links... + + + + + +{$data['body']} + +HTML; + $exectime = round(microtime(true)-$STARTTIME,4); + echo ''; + if (isLoggedIn()) echo ''; + echo ''; +} + +// ----------------------------------------------------------------------------------------------- +// Installation +// This function should NEVER be called if the file data/config.php exists. +function install() +{ + // FIXME: check version of php ? + if (isset($_POST['setlogin']) && isset($_POST['setpassword']) && isset($_POST['settimezone'])) + { + if ($_POST['setlogin']!='' && $_POST['setpassword']!='' && in_array($_POST['settimezone'],timezone_identifiers_list())) + { // Everything is ok, let's create config file. + $salt=sha1(uniqid('',true).'_'.mt_rand()); // Salt renders rainbow-tables attacks useless. + $hash = sha1($_POST['setpassword'].$_POST['setlogin'].$salt); + $config=''; + file_put_contents(CONFIG_FILE,$config); + echo ''; + exit; + } + } + // Display config form: + $timezones=''; + foreach(timezone_identifiers_list() as $tz) $timezones.='\n"; + echo <<Shaarli - Configuration

    Shaarli - Shaare your links...

    It looks like it's the first time you run Shaarli. Please chose a login/password and a timezone:
    +
    +Login:

    Password:

    +Timezone:

    +
    +HTML; + exit; +} + +$LINKSDB=new linkdb(isLoggedIn()); // Read links from database (and filter private links if used it not logged in). +if (!isset($_SESSION['LINKS_PER_PAGE'])) $_SESSION['LINKS_PER_PAGE']=LINKS_PER_PAGE; +if (startswith($_SERVER["QUERY_STRING"],'do=rss')) { showRSS(); exit; } +renderPage(); +?> \ No newline at end of file