]> git.immae.eu Git - github/shaarli/Shaarli.git/blobdiff - index.php
Link filter refactoring
[github/shaarli/Shaarli.git] / index.php
index 8f0bd4b92df1f761a1973f7bc435834c254dbb92..1664c01b6ec192d24a2a6155f651f5db3e12d1b2 100644 (file)
--- a/index.php
+++ b/index.php
@@ -1,6 +1,6 @@
 <?php
 /**
- * Shaarli v0.6.0 - Shaare your links...
+ * Shaarli v0.6.2 - Shaare your links...
  *
  * The personal, minimalist, super-fast, no-database Delicious clone.
  *
@@ -44,6 +44,9 @@ $GLOBALS['config']['DATASTORE'] = $GLOBALS['config']['DATADIR'].'/datastore.php'
 // Banned IPs
 $GLOBALS['config']['IPBANS_FILENAME'] = $GLOBALS['config']['DATADIR'].'/ipbans.php';
 
+// Access log
+$GLOBALS['config']['LOG_FILE'] = $GLOBALS['config']['DATADIR'].'/log.txt';
+
 // For updates check of Shaarli
 $GLOBALS['config']['UPDATECHECK_FILENAME'] = $GLOBALS['config']['DATADIR'].'/lastupdatecheck.txt';
 
@@ -52,7 +55,7 @@ $GLOBALS['config']['RAINTPL_TMP'] = 'tmp/';
 // Raintpl template directory (keep the trailing slash!)
 $GLOBALS['config']['RAINTPL_TPL'] = 'tpl/';
 
-// Thuumbnail cache directory
+// Thumbnail cache directory
 $GLOBALS['config']['CACHEDIR'] = 'cache';
 
 // Atom & RSS feed cache directory
@@ -89,7 +92,8 @@ $GLOBALS['config']['ENABLE_THUMBNAILS'] = true;
 $GLOBALS['config']['ENABLE_LOCALCACHE'] = true;
 
 // Update check frequency for Shaarli. 86400 seconds=24 hours
-$GLOBALS['config']['UPDATECHECK_INTERVAL'] = 86400 ;
+$GLOBALS['config']['UPDATECHECK_BRANCH'] = 'stable';
+$GLOBALS['config']['UPDATECHECK_INTERVAL'] = 86400;
 
 
 /*
@@ -115,7 +119,7 @@ $GLOBALS['config']['PUBSUBHUB_URL'] = '';
 /*
  * PHP configuration
  */
-define('shaarli_version', '0.6.0');
+define('shaarli_version', '0.6.2');
 
 // http://server.com/x/shaarli --> /shaarli/
 define('WEB_PATH', substr($_SERVER["REQUEST_URI"], 0, 1+strrpos($_SERVER["REQUEST_URI"], '/', 0)));
@@ -141,10 +145,13 @@ if (is_file($GLOBALS['config']['CONFIG_FILE'])) {
 }
 
 // Shaarli library
+require_once 'application/ApplicationUtils.php';
 require_once 'application/Cache.php';
 require_once 'application/CachedPage.php';
+require_once 'application/FileUtils.php';
 require_once 'application/HttpUtils.php';
 require_once 'application/LinkDB.php';
+require_once 'application/LinkFilter.php';
 require_once 'application/TimeZone.php';
 require_once 'application/Url.php';
 require_once 'application/Utils.php';
@@ -154,10 +161,10 @@ require_once 'application/Router.php';
 
 // Ensure the PHP version is supported
 try {
-    checkPHPVersion('5.3', PHP_VERSION);
-} catch(Exception $e) {
+    ApplicationUtils::checkPHPVersion('5.3', PHP_VERSION);
+} catch(Exception $exc) {
     header('Content-Type: text/plain; charset=utf-8');
-    echo $e->getMessage();
+    echo $exc->getMessage();
     exit;
 }
 
@@ -216,9 +223,6 @@ 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 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 '.escape(index_url($_SERVER));
 if (empty($GLOBALS['timezone'])) $GLOBALS['timezone']=date_default_timezone_get();
@@ -228,8 +232,24 @@ if (empty($GLOBALS['privateLinkByDefault'])) $GLOBALS['privateLinkByDefault']=fa
 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'])) {
+    // Ensure Shaarli has proper access to its resources
+    $errors = ApplicationUtils::checkResourcePermissions($GLOBALS['config']);
+
+    if ($errors != array()) {
+        $message = '<p>Insufficient permissions:</p><ul>';
+
+        foreach ($errors as $error) {
+            $message .= '<li>'.$error.'</li>';
+        }
+        $message .= '</ul>';
+
+        header('Content-Type: text/html; charset=utf-8');
+        echo $message;
+        exit;
+    }
+
+    // Display the installation form if no existing config is found
     install();
 }
 
@@ -248,11 +268,11 @@ header('Content-Type: text/html; charset=utf-8'); // We use UTF-8 for proper int
 //==================================================================================================
 
 function setup_login_state() {
-       $userIsLoggedIn = false; // By default, we do not consider the user as logged in;
-       $loginFailure = false; // If set to true, every attempt to authenticate the user will fail. This indicates that an important condition isn't met.
        if ($GLOBALS['config']['OPEN_SHAARLI']) {
-           $userIsLoggedIn = true;
+           return true;
        }
+       $userIsLoggedIn = false; // By default, we do not consider the user as logged in;
+       $loginFailure = false; // If set to true, every attempt to authenticate the user will fail. This indicates that an important condition isn't met.
        if (!isset($GLOBALS['login'])) {
            $userIsLoggedIn = false;  // Shaarli is not configured yet.
            $loginFailure = true;
@@ -287,56 +307,15 @@ function setup_login_state() {
 }
 $userIsLoggedIn = setup_login_state();
 
-// Checks if an update is available for Shaarli.
-// (at most once a day, and only for registered user.)
-// Output: '' = no new version.
-//         other= the available version.
-function checkUpdate()
-{
-    if (!isLoggedIn()) return ''; // Do not check versions for visitors.
-    if (empty($GLOBALS['config']['ENABLE_UPDATECHECK'])) return ''; // Do not check if the user doesn't want to.
-
-    // Get latest version number at most once a day.
-    if (!is_file($GLOBALS['config']['UPDATECHECK_FILENAME']) || (filemtime($GLOBALS['config']['UPDATECHECK_FILENAME'])<time()-($GLOBALS['config']['UPDATECHECK_INTERVAL'])))
-    {
-        $version = shaarli_version;
-        list($headers, $data) = get_http_url('https://raw.githubusercontent.com/shaarli/Shaarli/master/shaarli_version.php', 2);
-        if (strpos($headers[0], '200 OK') !== false) {
-            $version = str_replace(' */ ?>', '', str_replace('<?php /* ', '', $data));
-        }
-        // 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:
-    $newestversion=file_get_contents($GLOBALS['config']['UPDATECHECK_FILENAME']);
-    if (version_compare($newestversion,shaarli_version)==1) return $newestversion;
-    return '';
-}
-
 
 // -----------------------------------------------------------------------------------------------
 // Log to text file
 function logm($message)
 {
     $t = strval(date('Y/m/d_H:i:s')).' - '.$_SERVER["REMOTE_ADDR"].' - '.strval($message)."\n";
-    file_put_contents($GLOBALS['config']['DATADIR'].'/log.txt',$t,FILE_APPEND);
-}
-
-// 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:|magnet:)\S+[[:alnum:]]/?)!si','<a href="'.$redir.'$1" rel="nofollow">$1</a>',$url);
+    file_put_contents($GLOBALS['config']['LOG_FILE'], $t, FILE_APPEND);
 }
 
-// This function inserts &nbsp; where relevant so that multiple spaces are properly displayed in HTML
-// even in the absence of <pre>  (This is used in description to keep text formatting)
-function keepMultipleSpaces($text)
-{
-    return str_replace('  ',' &nbsp;',$text);
-
-}
 // ------------------------------------------------------------------------------------------
 // Sniff browser language to display dates in the right format automatically.
 // (Note that is may not work on your server if the corresponding local is not installed.)
@@ -654,7 +633,25 @@ class pageBuilder
     private function initialize()
     {
         $this->tpl = new RainTPL;
-        $this->tpl->assign('newversion', escape(checkUpdate()));
+
+        try {
+            $version = ApplicationUtils::checkUpdate(
+                shaarli_version,
+                $GLOBALS['config']['UPDATECHECK_FILENAME'],
+                $GLOBALS['config']['UPDATECHECK_INTERVAL'],
+                $GLOBALS['config']['ENABLE_UPDATECHECK'],
+                isLoggedIn(),
+                $GLOBALS['config']['UPDATECHECK_BRANCH']
+            );
+            $this->tpl->assign('newVersion', escape($version));
+            $this->tpl->assign('versionError', '');
+
+        } catch (Exception $exc) {
+            logm($exc->getMessage());
+            $this->tpl->assign('newVersion', '');
+            $this->tpl->assign('versionError', escape($exc->getMessage()));
+        }
+
         $this->tpl->assign('feedurl', escape(index_url($_SERVER)));
         $searchcrits = ''; // Search criteria
         if (!empty($_GET['searchtags'])) {
@@ -679,8 +676,8 @@ class pageBuilder
             $this->tpl->assign('pagetitle', $GLOBALS['pagetitle']);
         }
         $this->tpl->assign('shaarlititle', empty($GLOBALS['title']) ? 'Shaarli': $GLOBALS['title']);
-        if (!empty($GLOBALS['plugins']['errors'])) {
-            $this->tpl->assign('plugin_errors', $GLOBALS['plugins']['errors']);
+        if (!empty($GLOBALS['plugin_errors'])) {
+            $this->tpl->assign('plugin_errors', $GLOBALS['plugin_errors']);
         }
     }
 
@@ -727,24 +724,30 @@ function showRSS()
     // If cached was not found (or not usable), then read the database and build the response:
     $LINKSDB = new LinkDB(
         $GLOBALS['config']['DATASTORE'],
-        isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI'],
-        $GLOBALS['config']['HIDE_PUBLIC_LINKS']
+        isLoggedIn(),
+        $GLOBALS['config']['HIDE_PUBLIC_LINKS'],
+        $GLOBALS['redirector']
     );
     // Read links from database (and filter private links if user it not logged in).
 
     // Optionally filter the results:
-    $linksToDisplay=array();
-    if (!empty($_GET['searchterm'])) $linksToDisplay = $LINKSDB->filterFulltext($_GET['searchterm']);
-    else if (!empty($_GET['searchtags']))   $linksToDisplay = $LINKSDB->filterTags(trim($_GET['searchtags']));
-    else $linksToDisplay = $LINKSDB;
+    if (!empty($_GET['searchterm'])) {
+        $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']);
+    }
+    elseif (!empty($_GET['searchtags'])) {
+        $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags']));
+    }
+    else {
+        $linksToDisplay = $LINKSDB;
+    }
 
     $nblinksToDisplay = 50;  // Number of links to display.
-    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) ;
+    // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links.
+    if (!empty($_GET['nb'])) {
+        $nblinksToDisplay = $_GET['nb'] == 'all' ? count($linksToDisplay) : max(intval($_GET['nb']), 1);
     }
 
-    $pageaddr=escape(index_url($_SERVER));
+    $pageaddr = escape(index_url($_SERVER));
     echo '<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">';
     echo '<channel><title>'.$GLOBALS['title'].'</title><link>'.$pageaddr.'</link>';
     echo '<description>Shared links</description><language>en-en</language><copyright>'.$pageaddr.'</copyright>'."\n\n";
@@ -779,7 +782,9 @@ function showRSS()
         // 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($link['description']))).$descriptionlink.']]></description>'."\n</item>\n";
+        echo '<description><![CDATA['.
+            format_description($link['description'], $GLOBALS['redirector']) .
+            $descriptionlink . ']]></description>' . "\n</item>\n";
         $i++;
     }
     echo '</channel></rss><!-- Cached version of '.escape(page_url($_SERVER)).' -->';
@@ -816,20 +821,26 @@ function showATOM()
     // Read links from database (and filter private links if used it not logged in).
     $LINKSDB = new LinkDB(
         $GLOBALS['config']['DATASTORE'],
-        isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI'],
-        $GLOBALS['config']['HIDE_PUBLIC_LINKS']
+        isLoggedIn(),
+        $GLOBALS['config']['HIDE_PUBLIC_LINKS'],
+        $GLOBALS['redirector']
     );
 
     // Optionally filter the results:
-    $linksToDisplay=array();
-    if (!empty($_GET['searchterm'])) $linksToDisplay = $LINKSDB->filterFulltext($_GET['searchterm']);
-    else if (!empty($_GET['searchtags']))   $linksToDisplay = $LINKSDB->filterTags(trim($_GET['searchtags']));
-    else $linksToDisplay = $LINKSDB;
+    if (!empty($_GET['searchterm'])) {
+        $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']);
+    }
+    else if (!empty($_GET['searchtags'])) {
+        $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags']));
+    }
+    else {
+        $linksToDisplay = $LINKSDB;
+    }
 
     $nblinksToDisplay = 50;  // Number of links to display.
-    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) ;
+    // In URL, you can specificy the number of links. Example: nb=200 or nb=all for all links.
+    if (!empty($_GET['nb'])) {
+        $nblinksToDisplay = $_GET['nb']=='all' ? count($linksToDisplay) : max(intval($_GET['nb']), 1);
     }
 
     $pageaddr=escape(index_url($_SERVER));
@@ -858,7 +869,9 @@ function showATOM()
         if ($usepermalinks===true) $descriptionlink = '(<a href="'.$absurl.'">Link</a>)';
         if (strlen($link['description'])>0) $descriptionlink = '<br>'.$descriptionlink;
 
-        $entries.='<content type="html"><![CDATA['.nl2br(keepMultipleSpaces(text2clickable($link['description']))).$descriptionlink."]]></content>\n";
+        $entries .= '<content type="html"><![CDATA['.
+            format_description($link['description'], $GLOBALS['redirector']) .
+            $descriptionlink . "]]></content>\n";
         if ($link['tags']!='') // Adding tags to each ATOM entry (as mentioned in ATOM specification)
         {
             foreach(explode(' ',$link['tags']) as $tag)
@@ -910,8 +923,9 @@ function showDailyRSS() {
     // Read links from database (and filter private links if used it not logged in).
     $LINKSDB = new LinkDB(
         $GLOBALS['config']['DATASTORE'],
-        isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI'],
-        $GLOBALS['config']['HIDE_PUBLIC_LINKS']
+        isLoggedIn(),
+        $GLOBALS['config']['HIDE_PUBLIC_LINKS'],
+        $GLOBALS['redirector']
     );
 
     /* Some Shaarlies may have very few links, so we need to look
@@ -965,7 +979,7 @@ function showDailyRSS() {
         // We pre-format some fields for proper output.
         foreach ($linkdates as $linkdate) {
             $l = $LINKSDB[$linkdate];
-            $l['formatedDescription'] = nl2br(keepMultipleSpaces(text2clickable($l['description'])));
+            $l['formatedDescription'] = format_description($l['description'], $GLOBALS['redirector']);
             $l['thumbnail'] = thumbnail($l['url']);
             $l['timestamp'] = linkdate2timestamp($l['linkdate']);
             if (startsWith($l['url'], '?')) {
@@ -992,13 +1006,18 @@ function showDailyRSS() {
     exit;
 }
 
-// "Daily" page.
-function showDaily()
+/**
+ * Show the 'Daily' page.
+ *
+ * @param PageBuilder $pageBuilder Template engine wrapper.
+ */
+function showDaily($pageBuilder)
 {
     $LINKSDB = new LinkDB(
         $GLOBALS['config']['DATASTORE'],
-        isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI'],
-        $GLOBALS['config']['HIDE_PUBLIC_LINKS']
+        isLoggedIn(),
+        $GLOBALS['config']['HIDE_PUBLIC_LINKS'],
+        $GLOBALS['redirector']
     );
 
     $day=Date('Ymd',strtotime('-1 day')); // Yesterday, in format YYYYMMDD.
@@ -1016,7 +1035,7 @@ function showDaily()
     }
 
     try {
-        $linksToDisplay = $LINKSDB->filterDay($day);
+        $linksToDisplay = $LINKSDB->filter(LinkFilter::$FILTER_DAY, $day);
     } catch (Exception $exc) {
         error_log($exc);
         $linksToDisplay = array();
@@ -1029,7 +1048,7 @@ function showDaily()
         $taglist = explode(' ',$link['tags']);
         uasort($taglist, 'strcasecmp');
         $linksToDisplay[$key]['taglist']=$taglist;
-        $linksToDisplay[$key]['formatedDescription']=nl2br(keepMultipleSpaces(text2clickable($link['description'])));
+        $linksToDisplay[$key]['formatedDescription'] = format_description($link['description'], $GLOBALS['redirector']);
         $linksToDisplay[$key]['thumbnail'] = thumbnail($link['url']);
         $linksToDisplay[$key]['timestamp'] = linkdate2timestamp($link['linkdate']);
     }
@@ -1055,7 +1074,7 @@ function showDaily()
         array_push($columns[$index],$link); // Put entry in this column.
         $fill[$index]+=$length;
     }
-    $PAGE = new pageBuilder;
+
     $data = array(
         'linksToDisplay' => $linksToDisplay,
         'linkcount' => count($LINKSDB),
@@ -1068,10 +1087,10 @@ function showDaily()
     $pluginManager->executeHooks('render_daily', $data, array('loggedin' => isLoggedIn()));
 
     foreach ($data as $key => $value) {
-        $PAGE->assign($key, $value);
+        $pageBuilder->assign($key, $value);
     }
 
-    $PAGE->renderPage('daily');
+    $pageBuilder->renderPage('daily');
     exit;
 }
 
@@ -1088,8 +1107,9 @@ function renderPage()
 {
     $LINKSDB = new LinkDB(
         $GLOBALS['config']['DATASTORE'],
-        isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI'],
-        $GLOBALS['config']['HIDE_PUBLIC_LINKS']
+        isLoggedIn(),
+        $GLOBALS['config']['HIDE_PUBLIC_LINKS'],
+        $GLOBALS['redirector']
     );
 
     $PAGE = new pageBuilder;
@@ -1140,13 +1160,17 @@ function renderPage()
     if ($targetPage == Router::$PAGE_PICWALL)
     {
         // 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']));
-        else $links = $LINKSDB;
+        if (!empty($_GET['searchterm'])) {
+            $links = $LINKSDB->filter(LinkFilter::$FILTER_TEXT, $_GET['searchterm']);
+        }
+        elseif (! empty($_GET['searchtags'])) {
+            $links = $LINKSDB->filter(LinkFilter::$FILTER_TAG, trim($_GET['searchtags']));
+        }
+        else {
+            $links = $LINKSDB;
+        }
 
-        $body='';
-        $linksToDisplay=array();
+        $linksToDisplay = array();
 
         // Get only links which have a thumbnail.
         foreach($links as $link)
@@ -1204,6 +1228,19 @@ function renderPage()
         exit;
     }
 
+    // Daily page.
+    if ($targetPage == Router::$PAGE_DAILY) {
+        showDaily($PAGE);
+    }
+
+    // Display openseach plugin (XML)
+    if ($targetPage == Router::$PAGE_OPENSEARCH) {
+        header('Content-Type: application/xml; charset=utf-8');
+        $PAGE->assign('serverurl', index_url($_SERVER));
+        $PAGE->renderPage('opensearch');
+        exit;
+    }
+
     // -------- User clicks on a tag in a link: The tag is added to the list of searched tags (searchtags=...)
     if (isset($_GET['addtag']))
     {
@@ -1260,7 +1297,7 @@ function renderPage()
         }
 
         if (isset($params['searchtags'])) {
-            $tags = explode(' ',$params['searchtags']);
+            $tags = explode(' ', $params['searchtags']);
             $tags=array_diff($tags, array($_GET['removetag'])); // Remove value from array $tags.
             if (count($tags)==0) {
                 unset($params['searchtags']);
@@ -1445,7 +1482,8 @@ function renderPage()
         if (!empty($_POST['deletetag']) && !empty($_POST['fromtag']))
         {
             $needle=trim($_POST['fromtag']);
-            $linksToAlter = $LINKSDB->filterTags($needle,true); // True for case-sensitive tag search.
+            // True for case-sensitive tag search.
+            $linksToAlter = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $needle, true);
             foreach($linksToAlter as $key=>$value)
             {
                 $tags = explode(' ',trim($value['tags']));
@@ -1453,7 +1491,7 @@ function renderPage()
                 $value['tags']=trim(implode(' ',$tags));
                 $LINKSDB[$key]=$value;
             }
-            $LINKSDB->savedb($GLOBALS['config']['PAGECACHE']); // Save to disk.
+            $LINKSDB->savedb($GLOBALS['config']['PAGECACHE']);
             echo '<script>alert("Tag was removed from '.count($linksToAlter).' links.");document.location=\'?\';</script>';
             exit;
         }
@@ -1462,7 +1500,8 @@ function renderPage()
         if (!empty($_POST['renametag']) && !empty($_POST['fromtag']) && !empty($_POST['totag']))
         {
             $needle=trim($_POST['fromtag']);
-            $linksToAlter = $LINKSDB->filterTags($needle,true); // true for case-sensitive tag search.
+            // True for case-sensitive tag search.
+            $linksToAlter = $LINKSDB->filter(LinkFilter::$FILTER_TAG, $needle, true);
             foreach($linksToAlter as $key=>$value)
             {
                 $tags = explode(' ',trim($value['tags']));
@@ -1751,11 +1790,12 @@ HTML;
 // Process the import file form.
 function importFile()
 {
-    if (!(isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI'])) { die('Not allowed.'); }
+    if (!isLoggedIn()) { die('Not allowed.'); }
     $LINKSDB = new LinkDB(
         $GLOBALS['config']['DATASTORE'],
-        isLoggedIn() || $GLOBALS['config']['OPEN_SHAARLI'],
-        $GLOBALS['config']['HIDE_PUBLIC_LINKS']
+        isLoggedIn(),
+        $GLOBALS['config']['HIDE_PUBLIC_LINKS'],
+        $GLOBALS['redirector']
     );
     $filename=$_FILES['filetoupload']['name'];
     $filesize=$_FILES['filetoupload']['size'];
@@ -1842,82 +1882,78 @@ function importFile()
 function buildLinkList($PAGE,$LINKSDB)
 {
     // ---- Filter link database according to parameters
-    $linksToDisplay=array();
-    $search_type='';
-    $search_crits='';
-    if (isset($_GET['searchterm'])) // Fulltext search
-    {
-        $linksToDisplay = $LINKSDB->filterFulltext(trim($_GET['searchterm']));
-        $search_crits=escape(trim($_GET['searchterm']));
-        $search_type='fulltext';
-    }
-    elseif (isset($_GET['searchtags'])) // Search by tag
-    {
-        $linksToDisplay = $LINKSDB->filterTags(trim($_GET['searchtags']));
-        $search_crits=explode(' ',escape(trim($_GET['searchtags'])));
-        $search_type='tags';
-    }
-    elseif (isset($_SERVER['QUERY_STRING']) && preg_match('/[a-zA-Z0-9-_@]{6}(&.+?)?/',$_SERVER['QUERY_STRING'])) // Detect smallHashes in URL
-    {
-        $linksToDisplay = $LINKSDB->filterSmallHash(substr(trim($_SERVER["QUERY_STRING"], '/'),0,6));
-        if (count($linksToDisplay)==0)
-        {
-            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.';
+    $search_type = '';
+    $search_crits = '';
+    $privateonly = !empty($_SESSION['privateonly']) ? true : false;
+
+    // Fulltext search
+    if (isset($_GET['searchterm'])) {
+        $search_crits = escape(trim($_GET['searchterm']));
+        $search_type = LinkFilter::$FILTER_TEXT;
+        $linksToDisplay = $LINKSDB->filter($search_type, $search_crits, false, $privateonly);
+    }
+    // Search by tag
+    elseif (isset($_GET['searchtags'])) {
+        $search_crits = explode(' ', escape(trim($_GET['searchtags'])));
+        $search_type = LinkFilter::$FILTER_TAG;
+        $linksToDisplay = $LINKSDB->filter($search_type, $search_crits, false, $privateonly);
+    }
+    // Detect smallHashes in URL.
+    elseif (isset($_SERVER['QUERY_STRING'])
+        && preg_match('/[a-zA-Z0-9-_@]{6}(&.+?)?/', $_SERVER['QUERY_STRING'])) {
+        $search_type = LinkFilter::$FILTER_HASH;
+        $search_crits = substr(trim($_SERVER["QUERY_STRING"], '/'), 0, 6);
+        $linksToDisplay = $LINKSDB->filter($search_type, $search_crits);
+
+        if (count($linksToDisplay) == 0) {
+            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>Would you mind <a href="?">clicking here</a>?';
             exit;
         }
-        $search_type='permalink';
     }
-    else
-        $linksToDisplay = $LINKSDB;  // Otherwise, display without filtering.
-
-
-    // Option: Show only private links
-    if (!empty($_SESSION['privateonly']))
-    {
-        $tmp = array();
-        foreach($linksToDisplay as $linkdate=>$link)
-        {
-            if ($link['private']!=0) $tmp[$linkdate]=$link;
-        }
-        $linksToDisplay=$tmp;
+    // Otherwise, display without filtering.
+    else {
+        $linksToDisplay = $LINKSDB->filter('', '', false, $privateonly);
     }
 
     // ---- Handle paging.
-    /* Can someone explain to me why you get the following error when using array_keys() on an object which implements the interface ArrayAccess???
-       "Warning: array_keys() expects parameter 1 to be array, object given in ... "
-       If my class implements ArrayAccess, why won't array_keys() accept it ?  ( $keys=array_keys($linksToDisplay); )
-    */
-    $keys=array(); foreach($linksToDisplay as $key=>$value) { $keys[]=$key; } // Stupid and ugly. Thanks PHP.
+    $keys = array();
+    foreach ($linksToDisplay as $key => $value) {
+        $keys[] = $key;
+    }
 
     // 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'];
+    if (count($linksToDisplay) == 1) {
+        $GLOBALS['pagetitle'] = $linksToDisplay[$keys[0]]['title'].' - '.$GLOBALS['title'];
+    }
 
     // Select articles according to paging.
-    $pagecount = ceil(count($keys)/$_SESSION['LINKS_PER_PAGE']);
-    $pagecount = ($pagecount==0 ? 1 : $pagecount);
-    $page=( empty($_GET['page']) ? 1 : intval($_GET['page']));
-    $page = ( $page<1 ? 1 : $page );
-    $page = ( $page>$pagecount ? $pagecount : $page );
-    $i = ($page-1)*$_SESSION['LINKS_PER_PAGE']; // Start index.
-    $end = $i+$_SESSION['LINKS_PER_PAGE'];
-    $linkDisp=array(); // Links to display
+    $pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']);
+    $pagecount = $pagecount == 0 ? 1 : $pagecount;
+    $page= empty($_GET['page']) ? 1 : intval($_GET['page']);
+    $page = $page < 1 ? 1 : $page;
+    $page = $page > $pagecount ? $pagecount : $page;
+    // Start index.
+    $i = ($page-1) * $_SESSION['LINKS_PER_PAGE'];
+    $end = $i + $_SESSION['LINKS_PER_PAGE'];
+    $linkDisp = array();
     while ($i<$end && $i<count($keys))
     {
         $link = $linksToDisplay[$keys[$i]];
-        $link['description']=nl2br(keepMultipleSpaces(text2clickable($link['description'])));
-        $title=$link['title'];
-        $classLi =  $i%2!=0 ? '' : 'publicLinkHightLight';
-        $link['class'] = ($link['private']==0 ? $classLi : 'private');
-        $link['timestamp']=linkdate2timestamp($link['linkdate']);
-        $taglist = explode(' ',$link['tags']);
+        $link['description'] = format_description($link['description'], $GLOBALS['redirector']);
+        $classLi =  ($i % 2) != 0 ? '' : 'publicLinkHightLight';
+        $link['class'] = $link['private'] == 0 ? $classLi : 'private';
+        $link['timestamp'] = linkdate2timestamp($link['linkdate']);
+        $taglist = explode(' ', $link['tags']);
         uasort($taglist, 'strcasecmp');
-        $link['taglist']=$taglist;
+        $link['taglist'] = $taglist;
         $link['shorturl'] = smallHash($link['linkdate']);
-        if ($link["url"][0] === '?' && // Check for both signs of a note: starting with ? and 7 chars long. I doubt that you'll post any links that look like this.
-            strlen($link["url"]) === 7) {
-            $link["url"] = index_url($_SERVER) . $link["url"];
+        // Check for both signs of a note: starting with ? and 7 chars long.
+        if ($link['url'][0] === '?' &&
+            strlen($link['url']) === 7) {
+            $link['url'] = index_url($_SERVER) . $link['url'];
         }
 
         $linkDisp[$keys[$i]] = $link;
@@ -1925,13 +1961,21 @@ function buildLinkList($PAGE,$LINKSDB)
     }
 
     // Compute paging navigation
-    $searchterm= ( empty($_GET['searchterm']) ? '' : '&searchterm='.$_GET['searchterm'] );
-    $searchtags= ( empty($_GET['searchtags']) ? '' : '&searchtags='.$_GET['searchtags'] );
-    $paging='';
-    $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;
+    $searchterm = empty($_GET['searchterm']) ? '' : '&searchterm=' . $_GET['searchterm'];
+    $searchtags = empty($_GET['searchtags']) ? '' : '&searchtags=' . $_GET['searchtags'];
+    $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.
     $data = array(
@@ -1948,6 +1992,10 @@ function buildLinkList($PAGE,$LINKSDB)
         'links' => $linkDisp,
         'tags' => $LINKSDB->allTags(),
     );
+    // FIXME! temporary fix - see #399.
+    if (!empty($GLOBALS['pagetitle']) && count($linkDisp) == 1) {
+        $data['pagetitle'] = $GLOBALS['pagetitle'];
+    }
 
     $pluginManager = PluginManager::getInstance();
     $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => isLoggedIn()));
@@ -2213,45 +2261,6 @@ function install()
     exit;
 }
 
-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
-        }
-    }
-}
-
-
-
 /* 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 (e.g. a flickr page) and return the proper thumbnail.
@@ -2481,7 +2490,6 @@ 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($_SESSION['LINKS_PER_PAGE'])) $_SESSION['LINKS_PER_PAGE']=$GLOBALS['config']['LINKS_PER_PAGE'];
 renderPage();
 ?>