]> git.immae.eu Git - github/shaarli/Shaarli.git/blobdiff - index.php
fix quoting error introduced in 712501812b6f927b048b9d7f767cb15a370b3c81
[github/shaarli/Shaarli.git] / index.php
index 8436f8ac425c9eaba87d18bcf14a1dd58484b794..02a6725f918767b8164e91f2d5aa5a877d35a0c5 100644 (file)
--- a/index.php
+++ b/index.php
@@ -1,9 +1,15 @@
 <?php
-// Shaarli 0.0.40 beta - Shaare your links...
-// The personal, minimalist, super-fast, no-database delicious clone. By sebsauvage.net
+// Shaarli 0.0.42 beta - Shaare your links...
+// The personal, minimalist, super-fast, no-database Delicious clone. By sebsauvage.net
 // http://sebsauvage.net/wiki/doku.php?id=php:shaarli
 // Licence: http://www.opensource.org/licenses/zlib-license.php
-// Requires: php 5.1.x  (but autocomplete fields will only work if you have php 5.2.x)
+// Requires: PHP 5.1.x  (but autocomplete fields will only work if you have PHP 5.2.x)
+// -----------------------------------------------------------------------------------------------
+// NEVER TRUST IN PHP.INI
+// Some hosts do not define a default timezone in php.ini,
+// so we have to do this for avoid the strict standard error.
+date_default_timezone_set('UTC');
+
 // -----------------------------------------------------------------------------------------------
 // Hardcoded parameter (These parameters can be overwritten by creating the file /config/options.php)
 $GLOBALS['config']['DATADIR'] = 'data'; // Data subdirectory
@@ -15,26 +21,41 @@ $GLOBALS['config']['BAN_AFTER'] = 4;        // Ban IP after this many failures.
 $GLOBALS['config']['BAN_DURATION'] = 1800;  // Ban duration for IP address after login failures (in seconds) (1800 sec. = 30 minutes)
 $GLOBALS['config']['OPEN_SHAARLI'] = false; // If true, anyone can add/edit/delete links without having to login
 $GLOBALS['config']['HIDE_TIMESTAMPS'] = false; // If true, the moment when links were saved are not shown to users that are not logged in.
+$GLOBALS['config']['SHOW_ATOM'] = false; // If true, an extra "ATOM feed" button will be displayed in the toolbar
 $GLOBALS['config']['ENABLE_THUMBNAILS'] = true; // Enable thumbnails in links.
 $GLOBALS['config']['CACHEDIR'] = 'cache'; // Cache directory for thumbnails for SLOW services (like flickr)
 $GLOBALS['config']['PAGECACHE'] = 'pagecache'; // Page cache directory.
-$GLOBALS['config']['ENABLE_LOCALCACHE'] = true; // Enable Shaarli to store thumbnail in a local cache. Disable to reduce webspace usage.
+$GLOBALS['config']['ENABLE_LOCALCACHE'] = true; // Enable Shaarli to store thumbnail in a local cache. Disable to reduce web space usage.
 $GLOBALS['config']['PUBSUBHUB_URL'] = ''; // PubSubHubbub support. Put an empty string to disable, or put your hub url here to enable.
+$GLOBALS['config']['RAINTPL_TMP'] = 'tmp/' ; // Raintpl cache directory  (keep the trailing slash!)
+$GLOBALS['config']['RAINTPL_TPL'] = 'tpl/' ; // Raintpl template directory (keep the trailing slash!)
 $GLOBALS['config']['UPDATECHECK_FILENAME'] = $GLOBALS['config']['DATADIR'].'/lastupdatecheck.txt'; // For updates check of Shaarli.
 $GLOBALS['config']['UPDATECHECK_INTERVAL'] = 86400 ; // Updates check frequency for Shaarli. 86400 seconds=24 hours
                                           // Note: You must have publisher.php in the same directory as Shaarli index.php
+$GLOBALS['config']['ARCHIVE_ORG'] = false; // For each link, add a link to an archived version on archive.org
 // -----------------------------------------------------------------------------------------------
-// You should not touch below (or at your own risks !)
-// Optionnal config file.
+// You should not touch below (or at your own risks!)
+// Optional config file.
 if (is_file($GLOBALS['config']['DATADIR'].'/options.php')) require($GLOBALS['config']['DATADIR'].'/options.php');
 
-define('shaarli_version','0.0.40 beta');
-define('PHPPREFIX','<?php /* '); // Prefix to encapsulate data in php code.
-define('PHPSUFFIX',' */ ?>'); // Suffix to encapsulate data in php code.
+define('shaarli_version','0.0.42 beta');
+define('PHPPREFIX','<?php /* '); // Prefix to encapsulate data in PHP code.
+define('PHPSUFFIX',' */ ?>'); // Suffix to encapsulate data in PHP code.
+// http://server.com/x/shaarli --> /shaarli/
+define('WEB_PATH', substr($_SERVER["REQUEST_URI"], 0, 1+strrpos($_SERVER["REQUEST_URI"], '/', 0)));
 
 // Force cookie path (but do not change lifetime)
 $cookie=session_get_cookie_params();
-session_set_cookie_params($cookie['lifetime'],dirname($_SERVER["SCRIPT_NAME"]).'/'); // Default cookie expiration and path.
+$cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/';
+session_set_cookie_params($cookie['lifetime'],$cookiedir,$_SERVER['HTTP_HOST']); // Set default cookie expiration and path.
+
+// Set session parameters on server side.
+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 form using sessionID in URL if cookies are disabled.
+session_name('shaarli');
+if (session_id() == '') session_start();  // Start session if needed (Some server auto-start sessions).
 
 // PHP Settings
 ini_set('max_input_time','60');  // High execution time in case of problematic imports/exports.
@@ -46,9 +67,8 @@ error_reporting(E_ALL^E_WARNING);  // See all error except warnings.
 //error_reporting(-1); // See all errors (for debugging only)
 
 include "inc/rain.tpl.class.php"; //include Rain TPL
-raintpl::$tpl_dir = "tpl/"; // template directory
-if (!is_dir('tmp')) { mkdir('tmp',0705); chmod('tmp',0705); }
-raintpl::$cache_dir = "tmp/"; // cache directory
+raintpl::$tpl_dir = $GLOBALS['config']['RAINTPL_TPL']; // template directory
+raintpl::$cache_dir = $GLOBALS['config']['RAINTPL_TMP']; // cache directory
 
 ob_start();  // Output buffering for the page cache.
 
@@ -68,37 +88,37 @@ header("Cache-Control: no-store, no-cache, must-revalidate");
 header("Cache-Control: post-check=0, pre-check=0", false);
 header("Pragma: no-cache");
 
-// Directories creations (Note that your web host may require differents rights than 705.)
-if (!is_dir($GLOBALS['config']['DATADIR'])) { mkdir($GLOBALS['config']['DATADIR'],0705); chmod($GLOBALS['config']['DATADIR'],0705); }
-if (!is_dir('tmp')) { mkdir('tmp',0705); chmod('tmp',0705); } // For RainTPL temporary files.
-if (!is_file($GLOBALS['config']['DATADIR'].'/.htaccess')) { file_put_contents($GLOBALS['config']['DATADIR'].'/.htaccess',"Allow from none\nDeny from all\n"); } // Protect data files.
-if ($GLOBALS['config']['ENABLE_LOCALCACHE'])
-{
-    if (!is_dir($GLOBALS['config']['CACHEDIR'])) { mkdir($GLOBALS['config']['CACHEDIR'],0705); chmod($GLOBALS['config']['CACHEDIR'],0705); }
-    if (!is_file($GLOBALS['config']['CACHEDIR'].'/.htaccess')) { file_put_contents($GLOBALS['config']['CACHEDIR'].'/.htaccess',"Allow from none\nDeny from all\n"); } // Protect data files.
-}
-
-// Run config screen if first run:
-if (!is_file($GLOBALS['config']['CONFIG_FILE'])) install();
-
-require $GLOBALS['config']['CONFIG_FILE'];  // Read login/password hash into $GLOBALS.
+// Directories creations (Note that your web host may require different rights than 705.)
+if (!is_writable(realpath(dirname(__FILE__)))) die('<pre>ERROR: Shaarli does not have the right to write in its own directory.</pre>');
 
 // Handling of old config file which do not have the new parameters.
 if (empty($GLOBALS['title'])) $GLOBALS['title']='Shared links on '.htmlspecialchars(indexUrl());
 if (empty($GLOBALS['timezone'])) $GLOBALS['timezone']=date_default_timezone_get();
+if (empty($GLOBALS['redirector'])) $GLOBALS['redirector']='';
 if (empty($GLOBALS['disablesessionprotection'])) $GLOBALS['disablesessionprotection']=false;
+if (empty($GLOBALS['disablejquery'])) $GLOBALS['disablejquery']=false;
+if (empty($GLOBALS['privateLinkByDefault'])) $GLOBALS['privateLinkByDefault']=false;
+if (empty($GLOBALS['titleLink'])) $GLOBALS['titleLink']='?';
+// I really need to rewrite Shaarli with a proper configuation manager.
 
+// Run config screen if first run:
+if (!is_file($GLOBALS['config']['CONFIG_FILE'])) install();
+
+require $GLOBALS['config']['CONFIG_FILE'];  // Read login/password hash into $GLOBALS.
+
+// 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.
 header('Content-Type: text/html; charset=utf-8'); // We use UTF-8 for proper international characters handling.
 
-// Check php version
+// Check PHP version
 function checkphpversion()
 {
     if (version_compare(PHP_VERSION, '5.1.0') < 0)
     {
         header('Content-Type: text/plain; charset=utf-8');
-        echo 'Your server supports php '.PHP_VERSION.'. Shaarli requires at last php 5.1.0, and thus cannot run. Sorry.';
+        echo 'Your PHP version is obsolete! Shaarli requires at least php 5.1.0, and thus cannot run. Sorry. Your PHP version has known security vulnerabilities and should be updated as soon as possible.';
         exit;
     }
 }
@@ -115,9 +135,9 @@ function checkUpdate()
     if (!is_file($GLOBALS['config']['UPDATECHECK_FILENAME']) || (filemtime($GLOBALS['config']['UPDATECHECK_FILENAME'])<time()-($GLOBALS['config']['UPDATECHECK_INTERVAL'])))
     {
         $version=shaarli_version;
-        list($httpstatus,$headers,$data) = getHTTP('http://sebsauvage.net/files/shaarli_version.txt',2);
+        list($httpstatus,$headers,$data) = getHTTP('https://raw.githubusercontent.com/shaarli/Shaarli/master/shaarli_version.txt',2);
         if (strpos($httpstatus,'200 OK')!==false) $version=$data;
-        // If failed, nevermind. We don't want to bother the user with that.
+        // If failed, never mind. We don't want to bother the user with that.
         file_put_contents($GLOBALS['config']['UPDATECHECK_FILENAME'],$version); // touch file date
     }
     // Compare versions:
@@ -133,11 +153,11 @@ function checkUpdate()
 class pageCache
 {
     private $url; // Full URL of the page to cache (typically the value returned by pageUrl())
-    private $shouldBeCached; // boolean: Should this url be cached ?
-    private $filename; // Name of the cache file for this url
+    private $shouldBeCached; // boolean: Should this url be cached?
+    private $filename; // Name of the cache file for this url.
 
-    /* 
-         $url = url (typically the value returned by pageUrl())
+    /*
+         $url = URL (typically the value returned by pageUrl())
          $shouldBeCached = boolean. If false, the cache will be disabled.
     */
     public function __construct($url,$shouldBeCached)
@@ -145,7 +165,7 @@ class pageCache
         $this->url = $url;
         $this->filename = $GLOBALS['config']['PAGECACHE'].'/'.sha1($url).'.cache';
         $this->shouldBeCached = $shouldBeCached;
-    } 
+    }
 
     // If the page should be cached and a cached version exists,
     // returns the cached version (otherwise, return null).
@@ -160,7 +180,6 @@ class pageCache
     public function cache($page)
     {
         if (!$this->shouldBeCached) return;
-        if (!is_dir($GLOBALS['config']['PAGECACHE'])) { mkdir($GLOBALS['config']['PAGECACHE'],0705); chmod($GLOBALS['config']['PAGECACHE'],0705); }
         file_put_contents($this->filename,$page);
     }
 
@@ -171,9 +190,9 @@ class pageCache
         if (is_dir($GLOBALS['config']['PAGECACHE']))
         {
             $handler = opendir($GLOBALS['config']['PAGECACHE']);
-            if ($handle!==false)
+            if ($handler!==false)
             {
-                while (($filename = readdir($handler))!==false) 
+                while (($filename = readdir($handler))!==false)
                 {
                     if (endsWith($filename,'.cache')) { unlink($GLOBALS['config']['PAGECACHE'].'/'.$filename); }
                 }
@@ -199,8 +218,8 @@ function nl2br_escaped($html)
     return str_replace('>','&gt;',str_replace('<','&lt;',nl2br($html)));
 }
 
-/* Returns the small hash of a string
-   eg. smallHash('20111006_131924') --> yZH23w
+/* Returns the small hash of a string, using RFC 4648 base64url format
+   e.g. smallHash('20111006_131924') --> yZH23w
    Small hashes:
      - are unique (well, as unique as crc32, at last)
      - are always 6 characters long.
@@ -211,18 +230,15 @@ function nl2br_escaped($html)
 function smallHash($text)
 {
     $t = rtrim(base64_encode(hash('crc32',$text,true)),'=');
-    $t = str_replace('+','-',$t); // Get rid of characters which need encoding in URLs.
-    $t = str_replace('/','_',$t);
-    $t = str_replace('=','@',$t);
-    return $t;
+    return strtr($t, '+/', '-_');
 }
 
-// In a string, converts urls to clickable links.
+// In a string, converts URLs to clickable links.
 // Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722
 function text2clickable($url)
 {
     $redir = empty($GLOBALS['redirector']) ? '' : $GLOBALS['redirector'];
-    return preg_replace('!(((?:https?|ftp|file)://|apt:)\S+[[:alnum:]]/?)!si','<a href="'.$redir.'$1" rel="nofollow">$1</a>',$url);
+    return preg_replace('!(((?:https?|ftp|file)://|apt:|magnet:)\S+[[:alnum:]]/?)!si','<a href="'.$redir.'$1" rel="nofollow">$1</a>',$url);
 }
 
 // This function inserts &nbsp; where relevant so that multiple spaces are properly displayed in HTML
@@ -230,7 +246,7 @@ function text2clickable($url)
 function keepMultipleSpaces($text)
 {
     return str_replace('  ',' &nbsp;',$text);
-    
+
 }
 // ------------------------------------------------------------------------------------------
 // Sniff browser language to display dates in the right format automatically.
@@ -238,8 +254,8 @@ function keepMultipleSpaces($text)
 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 (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];
     }
     setlocale(LC_TIME,$loc);  // LC_TIME = Set local for date/time format only.
@@ -264,12 +280,6 @@ function pubsubhub()
 
 // ------------------------------------------------------------------------------------------
 // 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()
@@ -281,16 +291,20 @@ function allIPs()
     return $ip;
 }
 
+function fillSessionInfo() {
+       $_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']=$GLOBALS['login'];
+       $_SESSION['expires_on']=time()+INACTIVITY_TIMEOUT;  // Set session expiration.
+}
+
 // 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.
+               fillSessionInfo();
         logm('Login successful');
         return True;
     }
@@ -303,6 +317,13 @@ function isLoggedIn()
 {
     if ($GLOBALS['config']['OPEN_SHAARLI']) return true;
 
+    if (!isset($GLOBALS['login'])) return false;  // Shaarli is not configured yet.
+
+       if (@$_COOKIE['shaarli_staySignedIn']===STAY_SIGNED_IN_TOKEN)
+       {
+               fillSessionInfo();
+               return true;
+       }
     // If session does not exist on server side, or IP address has changed, or session has expired, logout.
     if (empty($_SESSION['uid']) || ($GLOBALS['disablesessionprotection']==false && $_SESSION['ip']!=allIPs()) || time()>=$_SESSION['expires_on'])
     {
@@ -316,7 +337,9 @@ function isLoggedIn()
 }
 
 // Force logout.
-function logout() { if (isset($_SESSION)) { unset($_SESSION['uid']); unset($_SESSION['ip']); unset($_SESSION['username']);}  }
+function logout() { if (isset($_SESSION)) { unset($_SESSION['uid']); unset($_SESSION['ip']); unset($_SESSION['username']); unset($_SESSION['privateonly']); }
+setcookie('shaarli_staySignedIn', FALSE, 0, WEB_PATH);
+}
 
 
 // ------------------------------------------------------------------------------------------
@@ -373,24 +396,28 @@ 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.
+    {   // Login/password is OK.
         ban_loginOk();
         // 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_set_cookie_params($_SESSION['longlastingsession'],dirname($_SERVER["SCRIPT_NAME"]).'/'); // Set session cookie expiration on client side
-            // Note: Never forget the trailing slash on the cookie path !
+
+            $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/';
+            session_set_cookie_params($_SESSION['longlastingsession'],$cookiedir,$_SERVER['HTTP_HOST']); // Set session cookie expiration on client side
+            // Note: Never forget the trailing slash on the cookie path!
             session_regenerate_id(true);  // Send cookie with new expiration date to browser.
         }
         else // Standard session expiration (=when browser closes)
         {
-            session_set_cookie_params(0,dirname($_SERVER["SCRIPT_NAME"]).'/'); // 0 means "When browser closes"
+            $cookiedir = ''; if(dirname($_SERVER['SCRIPT_NAME'])!='/') $cookiedir=dirname($_SERVER["SCRIPT_NAME"]).'/';
+            session_set_cookie_params(0,$cookiedir,$_SERVER['HTTP_HOST']); // 0 means "When browser closes"
             session_regenerate_id(true);
         }
         // Optional redirect after login:
-        if (isset($_GET['post'])) { header('Location: ?post='.urlencode($_GET['post']).(!empty($_GET['title'])?'&title='.urlencode($_GET['title']):'').(!empty($_GET['source'])?'&source='.urlencode($_GET['source']):'')); exit; }
+        if (isset($_GET['post'])) { header('Location: ?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']):'')); exit; }
         if (isset($_POST['returnurl']))
         {
             if (endsWith($_POST['returnurl'],'?do=login')) { header('Location: ?'); exit; } // Prevent loops over login screen.
@@ -401,7 +428,9 @@ if (isset($_POST['login']))
     else
     {
         ban_loginFailed();
-        echo '<script language="JavaScript">alert("Wrong login/password.");document.location=\'?do=login\';</script>'; // Redirect to login screen.
+        $redir = '';
+        if (isset($_GET['post'])) { $redir = '&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']):''); }
+        echo '<script language="JavaScript">alert("Wrong login/password.");document.location=\'?do=login'.$redir.'\';</script>'; // Redirect to login screen.
         exit;
     }
 }
@@ -410,30 +439,34 @@ if (isset($_POST['login']))
 // Misc utility functions:
 
 // Returns the server URL (including port and http/https), without path.
-// eg. "http://myserver.com:8080"
+// e.g. "http://myserver.com:8080"
 // You can append $_SERVER['SCRIPT_NAME'] to get the current script URL.
 function serverUrl()
 {
     $https = (!empty($_SERVER['HTTPS']) && (strtolower($_SERVER['HTTPS'])=='on')) || $_SERVER["SERVER_PORT"]=='443'; // HTTPS detection.
     $serverport = ($_SERVER["SERVER_PORT"]=='80' || ($https && $_SERVER["SERVER_PORT"]=='443') ? '' : ':'.$_SERVER["SERVER_PORT"]);
-    return 'http'.($https?'s':'').'://'.$_SERVER["SERVER_NAME"].$serverport;
+    return 'http'.($https?'s':'').'://'.$_SERVER['HTTP_HOST'].$serverport;
 }
 
 // Returns the absolute URL of current script, without the query.
-// (eg. http://sebsauvage.net/links/)
+// (e.g. http://sebsauvage.net/links/)
 function indexUrl()
 {
-    return serverUrl() . ($_SERVER["SCRIPT_NAME"] == '/index.php' ? '/' : $_SERVER["SCRIPT_NAME"]);
+    $scriptname = $_SERVER["SCRIPT_NAME"];
+    // If the script is named 'index.php', we remove it (for better looking URLs,
+    // e.g. http://mysite.com/shaarli/?abcde instead of http://mysite.com/shaarli/index.php?abcde)
+    if (endswith($scriptname,'index.php')) $scriptname = substr($scriptname,0,strlen($scriptname)-9);
+    return serverUrl() . $scriptname;
 }
 
 // Returns the absolute URL of current script, WITH the query.
-// (eg. http://sebsauvage.net/links/?toto=titi&spamspamspam=humbug)
+// (e.g. http://sebsauvage.net/links/?toto=titi&spamspamspam=humbug)
 function pageUrl()
 {
     return indexUrl().(!empty($_SERVER["QUERY_STRING"]) ? '?'.$_SERVER["QUERY_STRING"] : '');
 }
 
-// Convert post_max_size/upload_max_filesize (eg.'16M') parameters to bytes.
+// 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]);
@@ -454,7 +487,7 @@ function getMaxFileSize()
     $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)
+    // FIXME: Then convert back to readable notations ? (e.g. 2M instead of 2000000)
     return $maxsize;
 }
 
@@ -502,7 +535,7 @@ function linkdate2iso8601($linkdate)
 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,
+    // Note that if you use a locale which is not installed on your webserver,
     // the date will not be displayed in the chosen locale, but probably in US notation.
 }
 
@@ -524,10 +557,10 @@ function http_parse_headers_shaarli( $headers )
 }
 
 /* GET an URL.
-   Input: $url : url to get (http://...)
+   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") or error message
-                      [1] = associative array containing HTTP response headers (eg. echo getHTTP($url)[1]['Content-Type'])
+   Output: An array.  [0] = HTTP status message (e.g. "HTTP/1.1 200 OK") or error message
+                      [1] = associative array containing HTTP response headers (e.g. echo getHTTP($url)[1]['Content-Type'])
                       [2] = data
     Example: list($httpstatus,$headers,$data) = getHTTP('http://sebauvage.net/');
              if (strpos($httpstatus,'200 OK')!==false)
@@ -539,15 +572,15 @@ function getHTTP($url,$timeout=30)
 {
     try
     {
-        $options = array('http'=>array('method'=>'GET','timeout' => $timeout)); // Force network timeout
+        $options = array('http'=>array('method'=>'GET','timeout' => $timeout, 'user_agent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:23.0) Gecko/20100101 Firefox/23.0')); // Force network timeout
         $context = stream_context_create($options);
         $data=file_get_contents($url,false,$context,-1, 4000000); // We download at most 4 Mb from source.
         if (!$data) { return array('HTTP Error',array(),''); }
-        $httpStatus=$http_response_header[0]; // eg. "HTTP/1.1 200 OK"
+        $httpStatus=$http_response_header[0]; // e.g. "HTTP/1.1 200 OK"
         $responseHeaders=http_parse_headers_shaarli($http_response_header);
         return array($httpStatus,$responseHeaders,$data);
     }
-    catch (Exception $e)  // getHTTP *can* fail silentely (we don't care if the title cannot be fetched)
+    catch (Exception $e)  // getHTTP *can* fail silently (we don't care if the title cannot be fetched)
     {
         return array($e->getMessage(),'','');
     }
@@ -568,19 +601,19 @@ if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array();  // Token are atta
 // Returns a token.
 function getToken()
 {
-    $rnd = sha1(uniqid('',true).'_'.mt_rand());  // We generate a random string.
+    $rnd = sha1(uniqid('',true).'_'.mt_rand().$GLOBALS['salt']);  // 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.
+// 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 true; // Token is OK.
     }
     return false; // Wrong token, or already used.
 }
@@ -591,7 +624,7 @@ function tokenOk($token)
    p = new pageBuilder;
    p.assign('myfield','myvalue');
    p.renderPage('mytemplate');
-   
+
 */
 class pageBuilder
 {
@@ -600,11 +633,11 @@ class pageBuilder
     function __construct()
     {
         $this->tpl=false;
-    } 
+    }
 
     private function initialize()
-    {    
-        $this->tpl = new RainTPL;    
+    {
+        $this->tpl = new RainTPL;
         $this->tpl->assign('newversion',checkUpdate());
         $this->tpl->assign('feedurl',htmlspecialchars(indexUrl()));
         $searchcrits=''; // Search criteria
@@ -615,22 +648,23 @@ class pageBuilder
         $this->tpl->assign('version',shaarli_version);
         $this->tpl->assign('scripturl',indexUrl());
         $this->tpl->assign('pagetitle','Shaarli');
-        $this->tpl->assign('privateonly',!empty($_SESSION['privateonly'])); // Show only private links ?
+        $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'] );
-        return;    
+        return;
     }
-    
+
     // 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).
-    // eg. pb.renderPage('picwall')
+    // e.g. pb.renderPage('picwall')
     public function renderPage($page)
     {
         if ($this->tpl===false) $this->initialize(); // Lazy initialization
@@ -646,14 +680,14 @@ class pageBuilder
       echo $mylinks['20110826_161819']['title'];
       foreach($mylinks as $link)
          echo $link['title'].' at url '.$link['url'].' ; description:'.$link['description'];
-   
+
    Available keys:
        title : Title of the link
-       url : URL of the link. Can be absolute or relative. Relative URLs are permalinks (eg.'?m-ukcw')
+       url : URL of the link. Can be absolute or relative. Relative URLs are permalinks (e.g.'?m-ukcw')
        description : description of the entry
-       private : Is this link private ? 0=no, other value=yes
-       linkdate : date of the creation of this entry, in the form YYYYMMDD_HHMMSS (eg.'20110914_192317')
-       tags : tags attached to this entry (separated by spaces)                   
+       private : Is this link private? 0=no, other value=yes
+       linkdate : date of the creation of this entry, in the form YYYYMMDD_HHMMSS (e.g.'20110914_192317')
+       tags : tags attached to this entry (separated by spaces)
 
    We implement 3 interfaces:
      - ArrayAccess so that this object behaves like an associative array.
@@ -662,15 +696,15 @@ class pageBuilder
 */
 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 $links; // List of links (associative array. Key=linkdate (e.g. "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)
+    private $loggedin; // Is the user logged in? (used to filter private links)
 
     // Constructor:
     function __construct($isLoggedIn)
-    // Input : $isLoggedIn : is the used logged in ?
+    // Input : $isLoggedIn : is the user logged in?
     {
         $this->loggedin = $isLoggedIn;
         $this->checkdb(); // Make sure data file exists.
@@ -684,7 +718,7 @@ class linkdb implements Iterator, Countable, ArrayAccess
     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($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;
@@ -713,7 +747,7 @@ class linkdb implements Iterator, Countable, ArrayAccess
              $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');
+             $link = array('title'=>'My secret stuff... - Pastebin.com','url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=','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($GLOBALS['config']['DATASTORE'], PHPPREFIX.base64_encode(gzdeflate(serialize($this->links))).PHPSUFFIX); // Write database to disk
         }
@@ -747,19 +781,19 @@ class linkdb implements Iterator, Countable, ArrayAccess
         invalidateCaches();
     }
 
-    // Returns the link for a given URL (if it exists). false it does not exist.
+    // Returns the link for a given URL (if it exists). False if 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->filterFulltext('hollandais'));
+    // Case insensitive search among links (in the URLs, title and description). Returns filtered list of links.
+    // e.g. print_r($mydb->filterFulltext('hollandais'));
     public function filterFulltext($searchterms)
     {
         // FIXME: explode(' ',$searchterms) and perform a AND search.
-        // FIXME: accept double-quotes to search for a string "as is" ?
+        // FIXME: accept double-quotes to search for a string "as is"?
         $filtered=array();
         $s = strtolower($searchterms);
         foreach($this->links as $l)
@@ -776,7 +810,7 @@ class linkdb implements Iterator, Countable, ArrayAccess
 
     // 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'));
+    // e.g. print_r($mydb->filterTags('linux programming'));
     public function filterTags($tags,$casesensitive=false)
     {
         $t = str_replace(',',' ',($casesensitive?$tags:strtolower($tags)));
@@ -792,9 +826,9 @@ class linkdb implements Iterator, Countable, ArrayAccess
         return $filtered;
     }
 
-    // Filter by day. Day must be in the form 'YYYYMMDD' (eg. '20120125')
+    // Filter by day. Day must be in the form 'YYYYMMDD' (e.g. '20120125')
     // Sort order is: older articles first.
-    // eg. print_r($mydb->filterDay('20120125'));
+    // e.g. print_r($mydb->filterDay('20120125'));
     public function filterDay($day)
     {
         $filtered=array();
@@ -832,7 +866,7 @@ class linkdb implements Iterator, Countable, ArrayAccess
         arsort($tags); // Sort tags by usage (most used tag first)
         return $tags;
     }
-    
+
     // Returns the list of days containing articles (oldest first)
     // Output: An array containing days (in format YYYYMMDD).
     public function days()
@@ -849,24 +883,33 @@ class linkdb implements Iterator, Countable, ArrayAccess
 }
 
 // ------------------------------------------------------------------------------------------
-// Ouput the last 50 links in RSS 2.0 format.
+// 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
+    $usepermalinks = isset($_GET['permalinks']);
+
     // Cache system
     $query = $_SERVER["QUERY_STRING"];
     $cache = new pageCache(pageUrl(),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(isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI']);  // Read links from database (and filter private links if used it not logged in).
+    $LINKSDB=new linkdb(isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI']);  // Read links from database (and filter private links if user it not logged in).
 
-    // Optionnaly filter the results:
+    // Optionally filter the results:
     $linksToDisplay=array();
     if (!empty($_GET['searchterm'])) $linksToDisplay = $LINKSDB->filterFulltext($_GET['searchterm']);
     elseif (!empty($_GET['searchtags']))   $linksToDisplay = $LINKSDB->filterTags(trim($_GET['searchtags']));
     else $linksToDisplay = $LINKSDB;
+    $nblinksToDisplay = 50;  // Number of links to display.
+    if (!empty($_GET['nb']))  // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links.
+    {
+        $nblinksToDisplay = $_GET['nb']=='all' ? count($linksToDisplay) : max($_GET['nb']+0,1) ;
+    }
 
     $pageaddr=htmlspecialchars(indexUrl());
     echo '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">';
@@ -881,23 +924,32 @@ function showRSS()
     }
     $i=0;
     $keys=array(); foreach($linksToDisplay as $key=>$value) { $keys[]=$key; }  // No, I can't use array_keys().
-    while ($i<50 && $i<count($keys))
+    while ($i<$nblinksToDisplay && $i<count($keys))
     {
         $link = $linksToDisplay[$keys[$i]];
         $guid = $pageaddr.'?'.smallHash($link['linkdate']);
         $rfc822date = linkdate2rfc822($link['linkdate']);
         $absurl = htmlspecialchars($link['url']);
         if (startsWith($absurl,'?')) $absurl=$pageaddr.$absurl;  // make permalink URL absolute
-        echo '<item><title>'.htmlspecialchars($link['title']).'</title><guid>'.$guid.'</guid><link>'.$absurl.'</link>';
+        if ($usepermalinks===true)
+            echo '<item><title>'.htmlspecialchars($link['title']).'</title><guid isPermaLink="false">'.$guid.'</guid><link>'.$guid.'</link>';
+        else
+            echo '<item><title>'.htmlspecialchars($link['title']).'</title><guid isPermaLink="false">'.$guid.'</guid><link>'.$absurl.'</link>';
         if (!$GLOBALS['config']['HIDE_TIMESTAMPS'] || isLoggedIn()) echo '<pubDate>'.htmlspecialchars($rfc822date)."</pubDate>\n";
         if ($link['tags']!='') // Adding tags to each RSS entry (as mentioned in RSS specification)
         {
             foreach(explode(' ',$link['tags']) as $tag) { echo '<category domain="'.htmlspecialchars($pageaddr).'">'.htmlspecialchars($tag).'</category>'."\n"; }
         }
-        echo '<description><![CDATA['.nl2br(keepMultipleSpaces(text2clickable(htmlspecialchars($link['description'])))).']]></description>'."\n</item>\n";
+
+        // Add permalink in description
+        $descriptionlink = '(<a href="'.$guid.'">Permalink</a>)';
+        // If user wants permalinks first, put the final link in description
+        if ($usepermalinks===true) $descriptionlink = '(<a href="'.$absurl.'">Link</a>)';
+        if (strlen($link['description'])>0) $descriptionlink = '<br>'.$descriptionlink;
+        echo '<description><![CDATA['.nl2br(keepMultipleSpaces(text2clickable(htmlspecialchars($link['description'])))).$descriptionlink.']]></description>'."\n</item>\n";
         $i++;
     }
-    echo '</channel></rss>';
+    echo '</channel></rss><!-- Cached version of '.htmlspecialchars(pageUrl()).' -->';
 
     $cache->cache(ob_get_contents());
     ob_end_flush();
@@ -905,11 +957,15 @@ function showRSS()
 }
 
 // ------------------------------------------------------------------------------------------
-// Ouput the last 50 links in ATOM format.
+// 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']);
+
     // Cache system
     $query = $_SERVER["QUERY_STRING"];
     $cache = new pageCache(pageUrl(),startsWith($query,'do=atom') && !isLoggedIn());
@@ -919,18 +975,23 @@ function showATOM()
     $LINKSDB=new linkdb(isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI']);  // Read links from database (and filter private links if used it not logged in).
 
 
-    // Optionnaly filter the results:
+    // Optionally filter the results:
     $linksToDisplay=array();
     if (!empty($_GET['searchterm'])) $linksToDisplay = $LINKSDB->filterFulltext($_GET['searchterm']);
     elseif (!empty($_GET['searchtags']))   $linksToDisplay = $LINKSDB->filterTags(trim($_GET['searchtags']));
     else $linksToDisplay = $LINKSDB;
+    $nblinksToDisplay = 50;  // Number of links to display.
+    if (!empty($_GET['nb']))  // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links.
+    {
+        $nblinksToDisplay = $_GET['nb']=='all' ? count($linksToDisplay) : max($_GET['nb']+0,1) ;
+    }
 
     $pageaddr=htmlspecialchars(indexUrl());
     $latestDate = '';
     $entries='';
     $i=0;
     $keys=array(); foreach($linksToDisplay as $key=>$value) { $keys[]=$key; }  // No, I can't use array_keys().
-    while ($i<50 && $i<count($keys))
+    while ($i<$nblinksToDisplay && $i<count($keys))
     {
         $link = $linksToDisplay[$keys[$i]];
         $guid = $pageaddr.'?'.smallHash($link['linkdate']);
@@ -938,9 +999,20 @@ function showATOM()
         $latestDate = max($latestDate,$iso8601date);
         $absurl = htmlspecialchars($link['url']);
         if (startsWith($absurl,'?')) $absurl=$pageaddr.$absurl;  // make permalink URL absolute
-        $entries.='<entry><title>'.htmlspecialchars($link['title']).'</title><link href="'.$absurl.'" /><id>'.$guid.'</id>';
+        $entries.='<entry><title>'.htmlspecialchars($link['title']).'</title>';
+        if ($usepermalinks===true)
+            $entries.='<link href="'.$guid.'" /><id>'.$guid.'</id>';
+        else
+            $entries.='<link href="'.$absurl.'" /><id>'.$guid.'</id>';
         if (!$GLOBALS['config']['HIDE_TIMESTAMPS'] || isLoggedIn()) $entries.='<updated>'.htmlspecialchars($iso8601date).'</updated>';
-        $entries.='<content type="html">'.htmlspecialchars(nl2br(keepMultipleSpaces(text2clickable(htmlspecialchars($link['description'])))))."</content>\n";
+
+        // Add permalink in description
+        $descriptionlink = htmlspecialchars('(<a href="'.$guid.'">Permalink</a>)');
+        // If user wants permalinks first, put the final link in description
+        if ($usepermalinks===true) $descriptionlink = htmlspecialchars('(<a href="'.$absurl.'">Link</a>)');
+        if (strlen($link['description'])>0) $descriptionlink = '&lt;br&gt;'.$descriptionlink;
+
+        $entries.='<content type="html">'.htmlspecialchars(nl2br(keepMultipleSpaces(text2clickable(htmlspecialchars($link['description']))))).$descriptionlink."</content>\n";
         if ($link['tags']!='') // Adding tags to each ATOM entry (as mentioned in ATOM specification)
         {
             foreach(explode(' ',$link['tags']) as $tag)
@@ -962,9 +1034,9 @@ function showATOM()
     $feed.='<author><name>'.htmlspecialchars($pageaddr).'</name><uri>'.htmlspecialchars($pageaddr).'</uri></author>';
     $feed.='<id>'.htmlspecialchars($pageaddr).'</id>'."\n\n"; // Yes, I know I should use a real IRI (RFC3987), but the site URL will do.
     $feed.=$entries;
-    $feed.='</feed>';
+    $feed.='</feed><!-- Cached version of '.htmlspecialchars(pageUrl()).' -->';
     echo $feed;
-    
+
     $cache->cache(ob_get_contents());
     ob_end_flush();
     exit;
@@ -982,11 +1054,11 @@ function showDailyRSS()
     $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(isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI']);  // Read links from database (and filter private links if used it not logged in).
-    
+
     /* Some Shaarlies may have very few links, so we need to look
        back in time (rsort()) until we have enough days ($nb_of_days).
     */
-    $linkdates=array(); foreach($LINKSDB as $linkdate=>$value) { $linkdates[]=$linkdate; } 
+    $linkdates=array(); foreach($LINKSDB as $linkdate=>$value) { $linkdates[]=$linkdate; }
     rsort($linkdates);
     $nb_of_days=7; // We take 7 days.
     $today=Date('Ymd');
@@ -999,16 +1071,16 @@ function showDailyRSS()
             if (empty($days[$day])) $days[$day]=array();
             $days[$day][]=$linkdate;
         }
-        if (count($days)>$nb_of_days) break; // Have we collected enough days ?
+        if (count($days)>$nb_of_days) break; // Have we collected enough days?
     }
-    
+
     // Build the RSS feed.
     header('Content-Type: application/rss+xml; charset=utf-8');
     $pageaddr=htmlspecialchars(indexUrl());
     echo '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0">';
     echo '<channel><title>Daily - '.htmlspecialchars($GLOBALS['title']).'</title><link>'.$pageaddr.'</link>';
     echo '<description>Daily shared links</description><language>en-en</language><copyright>'.$pageaddr.'</copyright>'."\n";
-    
+
     foreach($days as $day=>$linkdates) // For each day.
     {
         $daydate = utf8_encode(strftime('%A %d, %B %Y',linkdate2timestamp($day.'_000000'))); // Full text date
@@ -1016,7 +1088,7 @@ function showDailyRSS()
         $absurl=htmlspecialchars(indexUrl().'?do=daily&day='.$day);  // Absolute URL of the corresponding "Daily" page.
         echo '<item><title>'.htmlspecialchars($GLOBALS['title'].' - '.$daydate).'</title><guid>'.$absurl.'</guid><link>'.$absurl.'</link>';
         echo '<pubDate>'.htmlspecialchars($rfc822date)."</pubDate>";
-        
+
         // Build the HTML body of this RSS entry.
         $html='';
         $href='';
@@ -1026,21 +1098,21 @@ function showDailyRSS()
         {
             $l = $LINKSDB[$linkdate];
             $l['formatedDescription']=nl2br(keepMultipleSpaces(text2clickable(htmlspecialchars($l['description']))));
-            $l['thumbnail'] = thumbnail($l['url']);  
-            $l['localdate']=linkdate2locale($l['linkdate']);            
+            $l['thumbnail'] = thumbnail($l['url']);
+            $l['localdate']=linkdate2locale($l['linkdate']);
             if (startsWith($l['url'],'?')) $l['url']=indexUrl().$l['url'];  // make permalink URL absolute
-            $links[$linkdate]=$l;    
+            $links[$linkdate]=$l;
         }
         // Then build the HTML for this day:
-        $tpl = new RainTPL;    
+        $tpl = new RainTPL;
         $tpl->assign('links',$links);
         $html = $tpl->draw('dailyrss',$return_string=true);
         echo "\n";
         echo '<description><![CDATA['.$html.']]></description>'."\n</item>\n\n";
 
-    }    
-    echo '</channel></rss>';
-    
+    }
+    echo '</channel></rss><!-- Cached version of '.htmlspecialchars(pageUrl()).' -->';
+
     $cache->cache(ob_get_contents());
     ob_end_flush();
     exit;
@@ -1054,12 +1126,12 @@ function showDaily()
 
     $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=''; 
+    $previousday='';
+    $nextday='';
     if ($i!==false)
     {
         if ($i>1) $previousday=$days[$i-1];
@@ -1070,14 +1142,16 @@ function showDaily()
     // We pre-format some fields for proper output.
     foreach($linksToDisplay as $key=>$link)
     {
-        $linksToDisplay[$key]['taglist']=explode(' ',$link['tags']);
+        $taglist = explode(' ',$link['tags']);
+        uasort($taglist, 'strcasecmp');
+        $linksToDisplay[$key]['taglist']=$taglist;
         $linksToDisplay[$key]['formatedDescription']=nl2br(keepMultipleSpaces(text2clickable(htmlspecialchars($link['description']))));
-        $linksToDisplay[$key]['thumbnail'] = thumbnail($link['url']);            
+        $linksToDisplay[$key]['thumbnail'] = thumbnail($link['url']);
     }
-    
+
     /* We need to spread the articles on 3 columns.
-       I did not want to use a javascript lib like http://masonry.desandro.com/
-       so I manually spread entries with a simple method: I roughly evaluate the 
+       I did not want to use a JavaScript lib like http://masonry.desandro.com/
+       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.
@@ -1087,7 +1161,7 @@ function showDaily()
         // 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.
+        // 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.
         // Then put in column which is the less filled:
@@ -1105,7 +1179,7 @@ function showDaily()
     $PAGE->assign('col3',$columns[2]);
     $PAGE->assign('day',utf8_encode(strftime('%A %d, %B %Y',linkdate2timestamp($day.'_000000'))));
     $PAGE->assign('previousday',$previousday);
-    $PAGE->assign('nextday',$nextday);    
+    $PAGE->assign('nextday',$nextday);
     $PAGE->renderPage('daily');
     exit;
 }
@@ -1140,7 +1214,7 @@ function renderPage()
     // -------- Picture wall
     if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=picwall'))
     {
-        // Optionnaly filter the results:
+        // Optionally filter the results:
         $links=array();
         if (!empty($_GET['searchterm'])) $links = $LINKSDB->filterFulltext($_GET['searchterm']);
         elseif (!empty($_GET['searchtags']))   $links = $LINKSDB->filterTags(trim($_GET['searchtags']));
@@ -1184,7 +1258,7 @@ function renderPage()
         $PAGE->assign('linkcount',count($LINKSDB));
         $PAGE->assign('tags',$tagList);
         $PAGE->renderPage('tagcloud');
-        exit;    
+        exit;
     }
 
     // -------- User clicks on a tag in a link: The tag is added to the list of searched tags (searchtags=...)
@@ -1193,7 +1267,25 @@ function renderPage()
         // Get previous URL (http_referer) and add the tag to the searchtags parameters in query.
         if (empty($_SERVER['HTTP_REFERER'])) { header('Location: ?searchtags='.urlencode($_GET['addtag'])); exit; } // In case browser does not send HTTP_REFERER
         parse_str(parse_url($_SERVER['HTTP_REFERER'],PHP_URL_QUERY), $params);
-        $params['searchtags'] = (empty($params['searchtags']) ?  trim($_GET['addtag']) : trim($params['searchtags']).' '.trim($_GET['addtag']));
+
+        // Check if this tag is already in the search query and ignore it if it is.
+        // Each tag is always separated by a space
+        $current_tags = explode(' ', $params['searchtags']);
+        $addtag = true;
+        foreach ($current_tags as $value) {
+            if ($value === $_GET['addtag']) {
+                $addtag = false;
+                break;
+            }
+        }
+        // Append the tag if necessary
+        if (empty($params['searchtags'])) {
+            $params['searchtags'] = trim($_GET['addtag']);
+        }
+        else if ($addtag) {
+            $params['searchtags'] = trim($params['searchtags']).' '.trim($_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;
@@ -1220,10 +1312,14 @@ function renderPage()
     if (isset($_GET['linksperpage']))
     {
         if (is_numeric($_GET['linksperpage'])) { $_SESSION['LINKS_PER_PAGE']=abs(intval($_GET['linksperpage'])); }
-        header('Location: '.(empty($_SERVER['HTTP_REFERER'])?'?':$_SERVER['HTTP_REFERER']));
+        // Make sure the referrer is Shaarli itself.
+        $referer = '?';
+        if (!empty($_SERVER['HTTP_REFERER']) && strcmp(parse_url($_SERVER['HTTP_REFERER'],PHP_URL_HOST),$_SERVER['HTTP_HOST'])==0)
+            $referer = $_SERVER['HTTP_REFERER'];
+        header('Location: '.$referer);
         exit;
     }
-    
+
     // -------- User wants to see only private links (toggle)
     if (isset($_GET['privateonly']))
     {
@@ -1235,24 +1331,36 @@ function renderPage()
         {
             unset($_SESSION['privateonly']); // See all links
         }
-        header('Location: '.(empty($_SERVER['HTTP_REFERER'])?'?':$_SERVER['HTTP_REFERER']));
+        // Make sure the referrer is Shaarli itself.
+        $referer = '?';
+        if (!empty($_SERVER['HTTP_REFERER']) && strcmp(parse_url($_SERVER['HTTP_REFERER'],PHP_URL_HOST),$_SERVER['HTTP_HOST'])==0)
+            $referer = $_SERVER['HTTP_REFERER'];
+        header('Location: '.$referer);
         exit;
     }
 
     // -------- Handle other actions allowed for non-logged in users:
     if (!isLoggedIn())
     {
-        // User tries to post new link but is not loggedin:
+        // User tries to post new link but is not logged in:
         // 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['source'])?'&source='.urlencode($_GET['source']):'')); // Redirect to login page, then back to post link.
+            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.
             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;
+               }
+
         $PAGE = new pageBuilder;
         buildLinkList($PAGE,$LINKSDB); // Compute list of links to display
         $PAGE->renderPage('linklist');
-        exit; // Never remove this one ! All operations below are reserved for logged in user.
+        exit; // Never remove this one! All operations below are reserved for logged in user.
     }
 
     // -------- All other functions are reserved for the registered user:
@@ -1273,7 +1381,7 @@ function renderPage()
         if ($GLOBALS['config']['OPEN_SHAARLI']) die('You are not supposed to change a password on an Open Shaarli.');
         if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword']))
         {
-            if (!tokenOk($_POST['token'])) die('Wrong token.'); // Go away !
+            if (!tokenOk($_POST['token'])) die('Wrong token.'); // Go away!
 
             // Make sure old password is correct.
             $oldhash = sha1($_POST['oldpassword'].$GLOBALS['login'].$GLOBALS['salt']);
@@ -1300,15 +1408,18 @@ 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 (isTZvalid($_POST['continent'],$_POST['city']))
                     $tz = $_POST['continent'].'/'.$_POST['city'];
             $GLOBALS['timezone'] = $tz;
             $GLOBALS['title']=$_POST['title'];
+            $GLOBALS['titleLink']=$_POST['titleLink'];
             $GLOBALS['redirector']=$_POST['redirector'];
             $GLOBALS['disablesessionprotection']=!empty($_POST['disablesessionprotection']);
+            $GLOBALS['disablejquery']=!empty($_POST['disablejquery']);
+            $GLOBALS['privateLinkByDefault']=!empty($_POST['privateLinkByDefault']);
             writeConfig();
             echo '<script language="JavaScript">alert("Configuration was saved.");document.location=\'?do=tools\';</script>';
             exit;
@@ -1321,7 +1432,7 @@ function renderPage()
             $PAGE->assign('title',htmlspecialchars( empty($GLOBALS['title']) ? '' : $GLOBALS['title'] , ENT_QUOTES));
             $PAGE->assign('redirector',htmlspecialchars( empty($GLOBALS['redirector']) ? '' : $GLOBALS['redirector'] , ENT_QUOTES));
             list($timezone_form,$timezone_js) = templateTZform($GLOBALS['timezone']);
-            $PAGE->assign('timezone_form',$timezone_form); // FIXME: put entire tz form generation in template ?
+            $PAGE->assign('timezone_form',$timezone_form); // FIXME: Put entire tz form generation in template?
             $PAGE->assign('timezone_js',$timezone_js);
             $PAGE->renderPage('configure');
             exit;
@@ -1345,7 +1456,7 @@ function renderPage()
         if (!empty($_POST['deletetag']) && !empty($_POST['fromtag']))
         {
             $needle=trim($_POST['fromtag']);
-            $linksToAlter = $LINKSDB->filterTags($needle,true); // true for case-sensitive tag search.
+            $linksToAlter = $LINKSDB->filterTags($needle,true); // True for case-sensitive tag search.
             foreach($linksToAlter as $key=>$value)
             {
                 $tags = explode(' ',trim($value['tags']));
@@ -1353,7 +1464,7 @@ function renderPage()
                 $value['tags']=trim(implode(' ',$tags));
                 $LINKSDB[$key]=$value;
             }
-            $LINKSDB->savedb(); // save to disk
+            $LINKSDB->savedb(); // Save to disk.
             echo '<script language="JavaScript">alert("Tag was removed from '.count($linksToAlter).' links.");document.location=\'?\';</script>';
             exit;
         }
@@ -1366,17 +1477,17 @@ function renderPage()
             foreach($linksToAlter as $key=>$value)
             {
                 $tags = explode(' ',trim($value['tags']));
-                $tags[array_search($needle,$tags)] = trim($_POST['totag']); // Remplace tags value.
+                $tags[array_search($needle,$tags)] = trim($_POST['totag']); // Replace tags value.
                 $value['tags']=trim(implode(' ',$tags));
                 $LINKSDB[$key]=$value;
             }
-            $LINKSDB->savedb(); // save to disk
+            $LINKSDB->savedb(); // Save to disk.
             echo '<script language="JavaScript">alert("Tag was renamed in '.count($linksToAlter).' links.");document.location=\'?searchtags='.urlencode($_POST['totag']).'\';</script>';
             exit;
         }
     }
 
-    // -------- User wants to add a link without using the bookmarklet: show form.
+    // -------- User wants to add a link without using the bookmarklet: Show form.
     if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=addlink'))
     {
         $PAGE = new pageBuilder;
@@ -1388,19 +1499,23 @@ 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 !
+        if (!tokenOk($_POST['token'])) die('Wrong token.'); // Go away!
         $tags = trim(preg_replace('/\s\s+/',' ', $_POST['lf_tags'])); // Remove multiple spaces.
         $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),
+        $url = trim($_POST['lf_url']);
+        if (!startsWith($url,'http:') && !startsWith($url,'https:') && !startsWith($url,'ftp:') && !startsWith($url,'magnet:') && !startsWith($url,'?'))
+            $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.
         $LINKSDB[$linkdate] = $link;
-        $LINKSDB->savedb(); // save to disk
+        $LINKSDB->savedb(); // Save to disk.
         pubsubhub();
 
         // If we are called from the bookmarklet, we must close the popup:
         if (isset($_GET['source']) && $_GET['source']=='bookmarklet') { echo '<script language="JavaScript">self.close();</script>'; exit; }
         $returnurl = ( isset($_POST['returnurl']) ? $_POST['returnurl'] : '?' );
+        $returnurl .= '#'.smallHash($linkdate);  // Scroll to the link which has been edited.
         header('Location: '.$returnurl); // After saving the link, redirect to the page the user was on.
         exit;
     }
@@ -1408,19 +1523,20 @@ function renderPage()
     // -------- 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 we are called from the bookmarklet, we must close the popup:
         if (isset($_GET['source']) && $_GET['source']=='bookmarklet') { echo '<script language="JavaScript">self.close();</script>'; exit; }
         $returnurl = ( isset($_POST['returnurl']) ? $_POST['returnurl'] : '?' );
+        $returnurl .= '#'.smallHash($_POST['lf_linkdate']);  // Scroll to the link which has been edited.
         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.
+    // -------- 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
+        // - confirmation is handled by JavaScript
         // - we are protected from XSRF by the token.
         $linkdate=$_POST['lf_linkdate'];
         unset($LINKSDB[$linkdate]);
@@ -1466,18 +1582,45 @@ function renderPage()
             $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;
+            $description = (empty($_GET['description']) ? '' : $_GET['description']); // Get description if it was provided in URL (by the bookmarklet). [Bronco added that]
+            $tags = (empty($_GET['tags']) ? '' : $_GET['tags'] ); // Get tags if it was provided in URL
+            $private = (!empty($_GET['private']) && $_GET['private'] === "1" ? 1 : 0); // Get private if it was provided in URL
             if (($url!='') && 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 this is an HTTP link, we try go get the page to extract 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 specified in either 1) HTTP response headers or 2) <head> in html
-                if (strpos($status,'200 OK')!==false) $title=html_entity_decode(html_extract_title($data),ENT_QUOTES,'UTF-8');
-
+                if (strpos($status,'200 OK')!==false)
+                                        {
+                        // Look for charset in html header.
+                                               preg_match('#<meta .*charset=.*>#Usi', $data, $meta);
+
+                                               // If found, extract encoding.
+                                               if (!empty($meta[0]))
+                                               {
+                                                       // Get encoding specified in header.
+                                                       preg_match('#charset="?(.*)"#si', $meta[0], $enc);
+                                                       // If charset not found, use utf-8.
+                                                       $html_charset = (!empty($enc[1])) ? strtolower($enc[1]) : 'utf-8';
+                                               }
+                                               else { $html_charset = 'utf-8'; }
+
+                                               // Extract title
+                                               $title = html_extract_title($data);
+                                               if (!empty($title))
+                                               {
+                                                       // Re-encode title in utf-8 if necessary.
+                                                       $title = ($html_charset == 'iso-8859-1') ? utf8_encode($title) : $title;
+                                               }
+                                       }
             }
-            if ($url=='') $url='?'.smallHash($linkdate); // In case of empty URL, this is just a text (with a link that point to itself)
-            $link = array('linkdate'=>$linkdate,'title'=>$title,'url'=>$url,'description'=>$description,'tags'=>$tags,'private'=>0);
+            if ($url=='') // In case of empty URL, this is just a text (with a link that points to itself)
+            {
+                $url='?'.smallHash($linkdate);
+                $title='Note: ';
+            }
+            $link = array('linkdate'=>$linkdate,'title'=>$title,'url'=>$url,'description'=>$description,'tags'=>$tags,'private'=>$private);
         }
 
         $PAGE = new pageBuilder;
@@ -1501,7 +1644,7 @@ function renderPage()
             exit;
         }
         $exportWhat=$_GET['what'];
-        if (!array_intersect(array('all','public','private'),array($exportWhat))) die('What are you trying to export ???');
+        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');
@@ -1574,8 +1717,8 @@ function importFile()
     $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 ?
+    $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:
@@ -1586,7 +1729,7 @@ function importFile()
     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.
+        // This format is supported by all browsers (except IE, of course), also Delicious, Diigo and others.
         foreach(explode('<DT>',$data) as $html) // explode is very fast
         {
             $link = array('linkdate'=>'','title'=>'','url'=>'','description'=>'','tags'=>'','private'=>0);
@@ -1602,7 +1745,11 @@ function importFile()
                 {
                     $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);
+                    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');
                 }
@@ -1616,14 +1763,14 @@ function importFile()
 
                        // 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.
+                       // 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.
+                    else // Link already present in database.
                     {
                         if ($overwrite)
                         {   // If overwrite is required, we import link data, except date/time.
@@ -1638,11 +1785,11 @@ function importFile()
         }
         $LINKSDB->savedb();
 
-        echo '<script language="JavaScript">alert("File '.$filename.' ('.$filesize.' bytes) was successfully processed: '.$import_count.' links imported.");document.location=\'?\';</script>';
+        echo '<script language="JavaScript">alert("File '.json_encode($filename).' ('.$filesize.' bytes) was successfully processed: '.$import_count.' links imported.");document.location=\'?\';</script>';
     }
     else
     {
-        echo '<script language="JavaScript">alert("File '.$filename.' ('.$filesize.' bytes) has an unknown file format. Nothing was imported.");document.location=\'?\';</script>';
+        echo '<script language="JavaScript">alert("File '.json_encode($filename).' ('.$filesize.' bytes) has an unknown file format. Nothing was imported.");document.location=\'?\';</script>';
     }
 }
 
@@ -1674,14 +1821,14 @@ function buildLinkList($PAGE,$LINKSDB)
         {
             header($_SERVER["SERVER_PROTOCOL"]." 404 Not Found");
             echo '<h1>404 Not found.</h1>Oh crap. The link you are trying to reach does not exist or has been deleted.';
-            echo '<br>You would mind <a href="?">clicking here</a> ?';
+            echo '<br>You would mind <a href="?">clicking here</a>?';
             exit;
         }
         $search_type='permalink';
     }
     else
-        $linksToDisplay = $LINKSDB;  // otherwise, display without filtering.
-    
+        $linksToDisplay = $LINKSDB;  // Otherwise, display without filtering.
+
     // Option: Show only private links
     if (!empty($_SESSION['privateonly']))
     {
@@ -1694,11 +1841,11 @@ function buildLinkList($PAGE,$LINKSDB)
     }
 
     // ---- 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 ???
+    /* 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.
+    $keys=array(); foreach($linksToDisplay as $key=>$value) { $keys[]=$key; } // Stupid and ugly. Thanks PHP.
 
     // If there is only a single link, we change on-the-fly the title of the page.
     if (count($linksToDisplay)==1) $GLOBALS['pagetitle'] = $linksToDisplay[$keys[0]]['title'].' - '.$GLOBALS['title'];
@@ -1720,11 +1867,13 @@ function buildLinkList($PAGE,$LINKSDB)
         $classLi =  $i%2!=0 ? '' : 'publicLinkHightLight';
         $link['class'] = ($link['private']==0 ? $classLi : 'private');
         $link['localdate']=linkdate2locale($link['linkdate']);
-        $link['taglist']=explode(' ',$link['tags']);
+        $taglist = explode(' ',$link['tags']);
+        uasort($taglist, 'strcasecmp');
+        $link['taglist']=$taglist;
         $linkDisp[$keys[$i]] = $link;
         $i++;
     }
-    
+
     // Compute paging navigation
     $searchterm= ( empty($_GET['searchterm']) ? '' : '&searchterm='.$_GET['searchterm'] );
     $searchtags= ( empty($_GET['searchtags']) ? '' : '&searchtags='.$_GET['searchtags'] );
@@ -1732,8 +1881,8 @@ function buildLinkList($PAGE,$LINKSDB)
     $previous_page_url=''; if ($i!=count($keys)) $previous_page_url='?page='.($page+1).$searchterm.$searchtags;
     $next_page_url='';if ($page>1) $next_page_url='?page='.($page-1).$searchterm.$searchtags;
 
-    $token = ''; if (isLoggedIn()) $token=getToken();   
+    $token = ''; if (isLoggedIn()) $token=getToken();
+
     // Fill all template fields.
     $PAGE->assign('linkcount',count($LINKSDB));
     $PAGE->assign('previous_page_url',$previous_page_url);
@@ -1742,18 +1891,18 @@ function buildLinkList($PAGE,$LINKSDB)
     $PAGE->assign('page_max',$pagecount);
     $PAGE->assign('result_count',count($linksToDisplay));
     $PAGE->assign('search_type',$search_type);
-    $PAGE->assign('search_crits',$search_crits);   
-    $PAGE->assign('redirector',empty($GLOBALS['redirector']) ? '' : $GLOBALS['redirector']); // optional redirector URL
+    $PAGE->assign('search_crits',$search_crits);
+    $PAGE->assign('redirector',empty($GLOBALS['redirector']) ? '' : $GLOBALS['redirector']); // Optional redirector URL.
     $PAGE->assign('token',$token);
     $PAGE->assign('links',$linkDisp);
     return;
 }
 
 // Compute the thumbnail for a link.
-// 
-// with a link to the original URL.
+//
+// With a link to the original URL.
 // Understands various services (youtube.com...)
-// Input: $url = url for which the thumbnail must be found.
+// Input: $url = URL for which the thumbnail must be found.
 //        $href = if provided, this URL will be followed instead of $url
 // Returns an associative array with thumbnail attributes (src,href,width,height,style,alt)
 // Some of them may be missing.
@@ -1764,44 +1913,44 @@ function computeThumbnail($url,$href=false)
     if ($href==false) $href=$url;
 
     // For most hosts, the URL of the thumbnail can be easily deduced from the URL of the link.
-    // (eg. http://www.youtube.com/watch?v=spVypYk4kto --->  http://img.youtube.com/vi/spVypYk4kto/default.jpg )
+    // (e.g. http://www.youtube.com/watch?v=spVypYk4kto --->  http://img.youtube.com/vi/spVypYk4kto/default.jpg )
     //                                     ^^^^^^^^^^^                                 ^^^^^^^^^^^
     $domain = parse_url($url,PHP_URL_HOST);
     if ($domain=='youtube.com' || $domain=='www.youtube.com')
     {
         parse_str(parse_url($url,PHP_URL_QUERY), $params); // Extract video ID and get thumbnail
-        if (!empty($params['v'])) return array('src'=>'http://img.youtube.com/vi/'.$params['v'].'/default.jpg',
+        if (!empty($params['v'])) return array('src'=>'https://img.youtube.com/vi/'.$params['v'].'/default.jpg',
                                                'href'=>$href,'width'=>'120','height'=>'90','alt'=>'YouTube thumbnail');
     }
     if ($domain=='youtu.be') // Youtube short links
     {
         $path = parse_url($url,PHP_URL_PATH);
-        return array('src'=>'http://img.youtube.com/vi'.$path.'/default.jpg',
-                     'href'=>$href,'width'=>'120','height'=>'90','alt'=>'YouTube thumbnail');        
+        return array('src'=>'https://img.youtube.com/vi'.$path.'/default.jpg',
+                     'href'=>$href,'width'=>'120','height'=>'90','alt'=>'YouTube thumbnail');
     }
     if ($domain=='pix.toile-libre.org') // pix.toile-libre.org image hosting
     {
         parse_str(parse_url($url,PHP_URL_QUERY), $params); // Extract image filename.
         if (!empty($params) && !empty($params['img'])) return array('src'=>'http://pix.toile-libre.org/upload/thumb/'.urlencode($params['img']),
-                                                                    'href'=>$href,'style'=>'max-width:120px; max-height:150px','alt'=>'pix.toile-libre.org thumbnail');    
-    }    
-    
+                                                                    'href'=>$href,'style'=>'max-width:120px; max-height:150px','alt'=>'pix.toile-libre.org thumbnail');
+    }
+
     if ($domain=='imgur.com')
     {
         $path = parse_url($url,PHP_URL_PATH);
         if (startsWith($path,'/a/')) return array(); // Thumbnails for albums are not available.
-        if (startsWith($path,'/r/')) return array('src'=>'http://i.imgur.com/'.basename($path).'s.jpg',
+        if (startsWith($path,'/r/')) return array('src'=>'https://i.imgur.com/'.basename($path).'s.jpg',
                                                   'href'=>$href,'width'=>'90','height'=>'90','alt'=>'imgur.com thumbnail');
-        if (startsWith($path,'/gallery/')) return array('src'=>'http://i.imgur.com'.substr($path,8).'s.jpg',
+        if (startsWith($path,'/gallery/')) return array('src'=>'https://i.imgur.com'.substr($path,8).'s.jpg',
                                                         'href'=>$href,'width'=>'90','height'=>'90','alt'=>'imgur.com thumbnail');
 
-        if (substr_count($path,'/')==1) return array('src'=>'http://i.imgur.com/'.substr($path,1).'s.jpg',
+        if (substr_count($path,'/')==1) return array('src'=>'https://i.imgur.com/'.substr($path,1).'s.jpg',
                                                      'href'=>$href,'width'=>'90','height'=>'90','alt'=>'imgur.com thumbnail');
     }
     if ($domain=='i.imgur.com')
     {
         $pi = pathinfo(parse_url($url,PHP_URL_PATH));
-        if (!empty($pi['filename'])) return array('src'=>'http://i.imgur.com/'.$pi['filename'].'s.jpg',
+        if (!empty($pi['filename'])) return array('src'=>'https://i.imgur.com/'.$pi['filename'].'s.jpg',
                                                   'href'=>$href,'width'=>'90','height'=>'90','alt'=>'imgur.com thumbnail');
     }
     if ($domain=='dailymotion.com' || $domain=='www.dailymotion.com')
@@ -1837,17 +1986,17 @@ function computeThumbnail($url,$href=false)
     )
     {
         if ($domain=='vimeo.com')
-        {   // Make sure this vimeo url points to a video (/xxx... where xxx is numeric)
+        {   // Make sure this vimeo URL points to a video (/xxx... where xxx is numeric)
             $path = parse_url($url,PHP_URL_PATH);
             if (!preg_match('!/\d+.+?!',$path)) return array(); // This is not a single video URL.
         }
         if ($domain=='xkcd.com' || endsWith($domain,'.xkcd.com'))
-        {   // Make sure this url points to a single comic (/xxx... where xxx is numeric)
+        {   // Make sure this URL points to a single comic (/xxx... where xxx is numeric)
             $path = parse_url($url,PHP_URL_PATH);
             if (!preg_match('!/\d+.+?!',$path)) return array();
         }
         if ($domain=='ted.com' || endsWith($domain,'.ted.com'))
-        {   // Make sure this TED url points to a video (/talks/...)
+        {   // Make sure this TED URL points to a video (/talks/...)
             $path = parse_url($url,PHP_URL_PATH);
             if ("/talks/" !== substr($path,0,7)) return array(); // This is not a single video URL.
         }
@@ -1864,7 +2013,7 @@ function computeThumbnail($url,$href=false)
     {
         $sign = hash_hmac('sha256', $url, $GLOBALS['salt']); // We use the salt to sign data (it's random, secret, and specific to each installation)
         return array('src'=>indexUrl().'?do=genthumbnail&hmac='.htmlspecialchars($sign).'&url='.urlencode($url),
-                     'href'=>$href,'width'=>'120','style'=>'height:auto;','alt'=>'thumbnail');        
+                     'href'=>$href,'width'=>'120','style'=>'height:auto;','alt'=>'thumbnail');
     }
     return array(); // No thumbnail.
 
@@ -1874,14 +2023,14 @@ function computeThumbnail($url,$href=false)
 // Returns the HTML code to display a thumbnail for a link
 // with a link to the original URL.
 // Understands various services (youtube.com...)
-// Input: $url = url for which the thumbnail must be found.
+// Input: $url = URL for which the thumbnail must be found.
 //        $href = if provided, this URL will be followed instead of $url
 // Returns '' if no thumbnail available.
 function thumbnail($url,$href=false)
 {
     $t = computeThumbnail($url,$href);
     if (count($t)==0) return ''; // Empty array = no thumbnail for this URL.
-    
+
     $html='<a href="'.htmlspecialchars($t['href']).'"><img src="'.htmlspecialchars($t['src']).'"';
     if (!empty($t['width']))  $html.=' width="'.htmlspecialchars($t['width']).'"';
     if (!empty($t['height'])) $html.=' height="'.htmlspecialchars($t['height']).'"';
@@ -1895,32 +2044,36 @@ function thumbnail($url,$href=false)
 // Returns the HTML code to display a thumbnail for a link
 // for the picture wall (using lazy image loading)
 // Understands various services (youtube.com...)
-// Input: $url = url for which the thumbnail must be found.
+// Input: $url = URL for which the thumbnail must be found.
 //        $href = if provided, this URL will be followed instead of $url
 // Returns '' if no thumbnail available.
 function lazyThumbnail($url,$href=false)
 {
-    $t = computeThumbnail($url,$href); 
+    $t = computeThumbnail($url,$href);
     if (count($t)==0) return ''; // Empty array = no thumbnail for this URL.
 
     $html='<a href="'.htmlspecialchars($t['href']).'">';
-    
-    // Lazy image (only loaded by javascript when in the viewport).
-    $html.='<img class="lazyimage" src="#" data-original="'.htmlspecialchars($t['src']).'"';
+
+    // Lazy image (only loaded by JavaScript when in the viewport).
+    if (!empty($GLOBALS['disablejquery'])) // (except if jQuery is disabled)
+        $html.='<img class="lazyimage" src="'.htmlspecialchars($t['src']).'"';
+    else
+        $html.='<img class="lazyimage" src="#" data-original="'.htmlspecialchars($t['src']).'"';
+
     if (!empty($t['width']))  $html.=' width="'.htmlspecialchars($t['width']).'"';
     if (!empty($t['height'])) $html.=' height="'.htmlspecialchars($t['height']).'"';
     if (!empty($t['style']))  $html.=' style="'.htmlspecialchars($t['style']).'"';
     if (!empty($t['alt']))    $html.=' alt="'.htmlspecialchars($t['alt']).'"';
     $html.='>';
-    
-    // No-javascript fallback:
+
+    // No-JavaScript fallback.
     $html.='<noscript><img src="'.htmlspecialchars($t['src']).'"';
     if (!empty($t['width']))  $html.=' width="'.htmlspecialchars($t['width']).'"';
     if (!empty($t['height'])) $html.=' height="'.htmlspecialchars($t['height']).'"';
     if (!empty($t['style']))  $html.=' style="'.htmlspecialchars($t['style']).'"';
     if (!empty($t['alt']))    $html.=' alt="'.htmlspecialchars($t['alt']).'"';
     $html.='></noscript></a>';
-    
+
     return $html;
 }
 
@@ -1931,7 +2084,31 @@ function lazyThumbnail($url,$href=false)
 function install()
 {
     // On free.fr host, make sure the /sessions directory exists, otherwise login will not work.
-    if (endsWith($_SERVER['SERVER_NAME'],'.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions',0705);
+    if (endsWith($_SERVER['HTTP_HOST'],'.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions',0705);
+
+
+    // This part makes sure sessions works correctly.
+    // (Because on some hosts, session.save_path may not be set correctly,
+    // or we may not have write access to it.)
+    if (isset($_GET['test_session']) && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working'))
+    {   // Step 2: Check if data in session is correct.
+        echo '<pre>Sessions do not seem to work correctly on your server.<br>';
+        echo 'Make sure the variable session.save_path is set correctly in your php config, and that you have write access to it.<br>';
+        echo 'It currently points to '.session_save_path().'<br>';
+        echo 'Check that the hostname used to access Shaarli contains a dot. On some browsers, accessing your server via a hostname like \'localhost\' or any custom hostname without a dot causes cookie storage to fail. We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>';
+        echo '<br><a href="?">Click to try again.</a></pre>';
+        die;
+    }
+    if (!isset($_SESSION['session_tested']))
+    {   // Step 1 : Try to store data in session and reload page.
+        $_SESSION['session_tested'] = 'Working';  // Try to set a variable in session.
+        header('Location: '.indexUrl().'?test_session');  // Redirect to check stored data.
+    }
+    if (isset($_GET['test_session']))
+    {   // Step 3: Sessions are OK. Remove test parameter from URL.
+        header('Location: '.indexUrl());
+    }
+
 
     if (!empty($_POST['setlogin']) && !empty($_POST['setpassword']))
     {
@@ -1946,14 +2123,14 @@ function install()
         $GLOBALS['hash'] = sha1($_POST['setpassword'].$GLOBALS['login'].$GLOBALS['salt']);
         $GLOBALS['title'] = (empty($_POST['title']) ? 'Shared links on '.htmlspecialchars(indexUrl()) : $_POST['title'] );
         writeConfig();
-        echo '<script language="JavaScript">alert("Shaarli is now configured. Please enter your login/password and start shaaring your links !");document.location=\'?do=login\';</script>';
+        echo '<script language="JavaScript">alert("Shaarli is now configured. Please enter your login/password and start shaaring your links!");document.location=\'?do=login\';</script>';
         exit;
     }
 
     // Display config form:
     list($timezone_form,$timezone_js) = templateTZform();
     $timezone_html=''; if ($timezone_form!='') $timezone_html='<tr><td valign="top"><b>Timezone:</b></td><td>'.$timezone_form.'</td></tr>';
-    
+
     $PAGE = new pageBuilder;
     $PAGE->assign('timezone_html',$timezone_html);
     $PAGE->assign('timezone_js',$timezone_js);
@@ -1961,14 +2138,14 @@ function install()
     exit;
 }
 
-// Generates the timezone selection form and javascript.
+// Generates the timezone selection form and JavaScript.
 // Input: (optional) current timezone (can be 'UTC/UTC'). It will be pre-selected.
 // Output: array(html,js)
 // Example: list($htmlform,$js) = templateTZform('Europe/Paris');  // Europe/Paris pre-selected.
-// Returns array('','') if server does not support timezones list. (eg. php 5.1 on free.fr)
+// Returns array('','') if server does not support timezones list. (e.g. PHP 5.1 on free.fr)
 function templateTZform($ptz=false)
 {
-    if (function_exists('timezone_identifiers_list')) // because of old php version (5.1) which can be found on free.fr
+    if (function_exists('timezone_identifiers_list')) // because of old PHP version (5.1) which can be found on free.fr
     {
         // Try to split the provided timezone.
         if ($ptz==false) { $l=timezone_identifiers_list(); $ptz=$l[0]; }
@@ -1977,7 +2154,7 @@ function templateTZform($ptz=false)
         // Display config form:
         $timezone_form = '';
         $timezone_js = '';
-        // The list is in the forme "Europe/Paris", "America/Argentina/Buenos_Aires"...
+        // The list is in the form "Europe/Paris", "America/Argentina/Buenos_Aires"...
         // We split the list in continents/cities.
         $continents = array();
         $cities = array();
@@ -1990,16 +2167,16 @@ function templateTZform($ptz=false)
                 $continent=substr($tz,0,$spos); $city=substr($tz,$spos+1);
                 $continents[$continent]=1;
                 if (!isset($cities[$continent])) $cities[$continent]='';
-                $cities[$continent].='<option value="'.$city.'"'.($pcity==$city?'selected':'').'>'.$city.'</option>';
+                $cities[$continent].='<option value="'.$city.'"'.($pcity==$city?' selected':'').'>'.$city.'</option>';
             }
         }
         $continents_html = '';
         $continents = array_keys($continents);
         foreach($continents as $continent)
-            $continents_html.='<option  value="'.$continent.'"'.($pcontinent==$continent?'selected':'').'>'.$continent.'</option>';
+            $continents_html.='<option  value="'.$continent.'"'.($pcontinent==$continent?' selected':'').'>'.$continent.'</option>';
         $cities_html = $cities[$pcontinent];
-        $timezone_form = "Continent: <select name=\"continent\" id=\"continent\" onChange=\"onChangecontinent();\">${continents_html}</select><br /><br />";
-        $timezone_form .= "City: <select name=\"city\" id=\"city\">${cities[$pcontinent]}</select><br /><br />";
+        $timezone_form = "Continent: <select name=\"continent\" id=\"continent\" onChange=\"onChangecontinent();\">${continents_html}</select>";
+        $timezone_form .= "&nbsp;&nbsp;&nbsp;&nbsp;City: <select name=\"city\" id=\"city\">${cities[$pcontinent]}</select><br />";
         $timezone_js = "<script language=\"JavaScript\">";
         $timezone_js .= "function onChangecontinent(){document.getElementById(\"city\").innerHTML = citiescontinent[document.getElementById(\"continent\").value];}";
         $timezone_js .= "var citiescontinent = ".json_encode($cities).";" ;
@@ -2015,17 +2192,52 @@ function templateTZform($ptz=false)
 function isTZvalid($continent,$city)
 {
     $tz = $continent.'/'.$city;
-    if (function_exists('timezone_identifiers_list')) // because of old php version (5.1) which can be found on free.fr
+    if (function_exists('timezone_identifiers_list')) // because of old PHP version (5.1) which can be found on free.fr
     {
-        if (in_array($tz, timezone_identifiers_list())) // it's a valid timezone ?
+        if (in_array($tz, timezone_identifiers_list())) // it's a valid timezone?
                     return true;
     }
     return false;
 }
-
+if (!function_exists('json_encode')) {
+    function json_encode($data) {
+        switch ($type = gettype($data)) {
+            case 'NULL':
+                return 'null';
+            case 'boolean':
+                return ($data ? 'true' : 'false');
+            case 'integer':
+            case 'double':
+            case 'float':
+                return $data;
+            case 'string':
+                return '"' . addslashes($data) . '"';
+            case 'object':
+                $data = get_object_vars($data);
+            case 'array':
+                $output_index_count = 0;
+                $output_indexed = array();
+                $output_associative = array();
+                foreach ($data as $key => $value) {
+                    $output_indexed[] = json_encode($value);
+                    $output_associative[] = json_encode($key) . ':' . json_encode($value);
+                    if ($output_index_count !== NULL && $output_index_count++ !== $key) {
+                        $output_index_count = NULL;
+                    }
+                }
+                if ($output_index_count !== NULL) {
+                    return '[' . implode(',', $output_indexed) . ']';
+                } else {
+                    return '{' . implode(',', $output_associative) . '}';
+                }
+            default:
+                return ''; // Not supported
+        }
+    }
+}
 
 // Webservices (for use with jQuery/jQueryUI)
-// eg.  index.php?ws=tags&term=minecr
+// e.g. index.php?ws=tags&term=minecr
 function processWS()
 {
     if (empty($_GET['ws']) || empty($_GET['term'])) return;
@@ -2033,7 +2245,7 @@ function processWS()
     $LINKSDB=new linkdb(isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI']);  // Read links from database (and filter private links if used it not logged in).
     header('Content-Type: application/json; charset=utf-8');
 
-    // Search in tags (case insentitive, cumulative search)
+    // Search in tags (case insensitive, cumulative search)
     if ($_GET['ws']=='tags')
     {
         $tags=explode(' ',str_replace(',',' ',$term)); $last = array_pop($tags); // Get the last term ("a b c d" ==> "a b c", "d")
@@ -2049,7 +2261,7 @@ function processWS()
         exit;
     }
 
-    // Search a single tag (case sentitive, single tag search)
+    // Search a single tag (case sensitive, single tag search)
     if ($_GET['ws']=='singletag')
     {
         /* To speed up things, we store list of tags in session */
@@ -2065,17 +2277,18 @@ function processWS()
 
 // Re-write configuration file according to globals.
 // Requires some $GLOBALS to be set (login,hash,salt,title).
-// If the config file cannot be saved, an error message is dislayed and the user is redirected to "Tools" menu.
+// If the config file cannot be saved, an error message is displayed and the user is redirected to "Tools" menu.
 // (otherwise, the function simply returns.)
 function writeConfig()
 {
     if (is_file($GLOBALS['config']['CONFIG_FILE']) && !isLoggedIn()) die('You are not authorized to alter config.'); // Only logged in user can alter config.
-    if (empty($GLOBALS['redirector'])) $GLOBALS['redirector']='';
-    if (empty($GLOBALS['disablesessionprotection'])) $GLOBALS['disablesessionprotection']=false;
     $config='<?php $GLOBALS[\'login\']='.var_export($GLOBALS['login'],true).'; $GLOBALS[\'hash\']='.var_export($GLOBALS['hash'],true).'; $GLOBALS[\'salt\']='.var_export($GLOBALS['salt'],true).'; ';
     $config .='$GLOBALS[\'timezone\']='.var_export($GLOBALS['timezone'],true).'; date_default_timezone_set('.var_export($GLOBALS['timezone'],true).'); $GLOBALS[\'title\']='.var_export($GLOBALS['title'],true).';';
+    $config .= '$GLOBALS[\'titleLink\']='.var_export($GLOBALS['titleLink'],true).'; ';
     $config .= '$GLOBALS[\'redirector\']='.var_export($GLOBALS['redirector'],true).'; ';
     $config .= '$GLOBALS[\'disablesessionprotection\']='.var_export($GLOBALS['disablesessionprotection'],true).'; ';
+    $config .= '$GLOBALS[\'disablejquery\']='.var_export($GLOBALS['disablejquery'],true).'; ';
+    $config .= '$GLOBALS[\'privateLinkByDefault\']='.var_export($GLOBALS['privateLinkByDefault'],true).'; ';
     $config .= ' ?>';
     if (!file_put_contents($GLOBALS['config']['CONFIG_FILE'],$config) || strcmp(file_get_contents($GLOBALS['config']['CONFIG_FILE']),$config)!=0)
     {
@@ -2084,12 +2297,12 @@ function writeConfig()
     }
 }
 
-/* Because some f*cking services like Flickr require an extra HTTP request to get the thumbnail URL,
+/* Because some f*cking services like flickr require an extra HTTP request to get the thumbnail URL,
    I have deported the thumbnail URL code generation here, otherwise this would slow down page generation.
-   The following function takes the URL a link (eg. a flickr page) and return the proper thumbnail.
-   This function is called by passing the url:
+   The following function takes the URL a link (e.g. a flickr page) and return the proper thumbnail.
+   This function is called by passing the URL:
    http://mywebsite.com/shaarli/?do=genthumbnail&hmac=[HMAC]&url=[URL]
-   [URL] is the URL of the link (eg. a flickr page)
+   [URL] is the URL of the link (e.g. a flickr page)
    [HMAC] is the signature for the [URL] (so that these URL cannot be forged).
    The function below will fetch the image from the webservice and store it in the cache.
 */
@@ -2097,7 +2310,7 @@ function genThumbnail()
 {
     // Make sure the parameters in the URL were generated by us.
     $sign = hash_hmac('sha256', $_GET['url'], $GLOBALS['salt']);
-    if ($sign!=$_GET['hmac']) die('Naughty boy !');
+    if ($sign!=$_GET['hmac']) die('Naughty boy!');
 
     // Let's see if we don't already have the image for this URL in the cache.
     $thumbname=hash('sha1',$_GET['url']).'.jpg';
@@ -2122,22 +2335,22 @@ function genThumbnail()
 
     if ($domain=='flickr.com' || endsWith($domain,'.flickr.com'))
     {
-        // Crude replacement to handle new Flickr domain policy (They prefer www. now)
+        // Crude replacement to handle new flickr domain policy (They prefer www. now)
         $url = str_replace('http://flickr.com/','http://www.flickr.com/',$url);
 
         // Is this a link to an image, or to a flickr page ?
         $imageurl='';
         if (endswith(parse_url($url,PHP_URL_PATH),'.jpg'))
-        {  // This is a direct link to an image. eg. http://farm1.staticflickr.com/5/5921913_ac83ed27bd_o.jpg
+        {  // This is a direct link to an image. e.g. http://farm1.staticflickr.com/5/5921913_ac83ed27bd_o.jpg
             preg_match('!(http://farm\d+\.staticflickr\.com/\d+/\d+_\w+_)\w.jpg!',$url,$matches);
             if (!empty($matches[1])) $imageurl=$matches[1].'m.jpg';
         }
-        else // this is a flickr page (html)
+        else // This is a flickr page (html)
         {
             list($httpstatus,$headers,$data) = getHTTP($url,20); // Get the flickr html page.
             if (strpos($httpstatus,'200 OK')!==false)
             {
-                // Flickr now nicely provides the URL of the thumbnail in each flickr page.
+                // flickr now nicely provides the URL of the thumbnail in each flickr page.
                 preg_match('!<link rel=\"image_src\" href=\"(.+?)\"!',$data,$matches);
                 if (!empty($matches[1])) $imageurl=$matches[1];
 
@@ -2168,9 +2381,9 @@ function genThumbnail()
     elseif ($domain=='vimeo.com' )
     {
         // This is more complex: we have to perform a HTTP request, then parse the result.
-        // Maybe we should deport this to javascript ? Example: http://stackoverflow.com/questions/1361149/get-img-thumbnails-from-vimeo/4285098#4285098
+        // Maybe we should deport this to JavaScript ? Example: http://stackoverflow.com/questions/1361149/get-img-thumbnails-from-vimeo/4285098#4285098
         $vid = substr(parse_url($url,PHP_URL_PATH),1);
-        list($httpstatus,$headers,$data) = getHTTP('http://vimeo.com/api/v2/video/'.htmlspecialchars($vid).'.php',5);
+        list($httpstatus,$headers,$data) = getHTTP('https://vimeo.com/api/v2/video/'.htmlspecialchars($vid).'.php',5);
         if (strpos($httpstatus,'200 OK')!==false)
         {
             $t = unserialize($data);
@@ -2192,7 +2405,7 @@ function genThumbnail()
         // The thumbnail for TED talks is located in the <link rel="image_src" [...]> tag on that page
         // http://www.ted.com/talks/mikko_hypponen_fighting_viruses_defending_the_net.html
         // <link rel="image_src" href="http://images.ted.com/images/ted/28bced335898ba54d4441809c5b1112ffaf36781_389x292.jpg" />
-        list($httpstatus,$headers,$data) = getHTTP($url,5); 
+        list($httpstatus,$headers,$data) = getHTTP($url,5);
         if (strpos($httpstatus,'200 OK')!==false)
         {
             // Extract the link to the thumbnail
@@ -2215,7 +2428,7 @@ function genThumbnail()
             }
         }
     }
-    
+
     elseif ($domain=='xkcd.com' || endsWith($domain,'.xkcd.com'))
     {
         // There is no thumbnail available for xkcd comics, so download the whole image and resize it.
@@ -2243,7 +2456,7 @@ function genThumbnail()
                 }
             }
         }
-    }    
+    }
 
     else
     {
@@ -2298,12 +2511,13 @@ function resizeImage($filepath)
     imagejpeg($im2, $tempname, 90);
     imagedestroy($im);
     imagedestroy($im2);
+    unlink($filepath);
     rename($tempname,$filepath);  // Overwrite original picture with thumbnail.
     return true;
 }
 
 // Invalidate caches when the database is changed or the user logs out.
-// (eg. tags cache).
+// (e.g. tags cache).
 function invalidateCaches()
 {
     unset($_SESSION['tags']);  // Purge cache attached to session.
@@ -2314,8 +2528,8 @@ if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=g
 if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=rss')) { showRSS(); exit; }
 if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=atom')) { showATOM(); exit; }
 if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=dailyrss')) { showDailyRSS(); exit; }
-if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=daily')) { showDaily(); exit; }    
+if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'do=daily')) { showDaily(); exit; }
 if (isset($_SERVER["QUERY_STRING"]) && startswith($_SERVER["QUERY_STRING"],'ws=')) { processWS(); exit; } // Webservices (for jQuery/jQueryUI)
 if (!isset($_SESSION['LINKS_PER_PAGE'])) $_SESSION['LINKS_PER_PAGE']=$GLOBALS['config']['LINKS_PER_PAGE'];
 renderPage();
-?>
\ No newline at end of file
+?>