aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.gitignore5
-rw-r--r--Makefile2
-rw-r--r--application/ApplicationUtils.php104
-rw-r--r--application/LinkDB.php20
-rw-r--r--application/Utils.php53
-rwxr-xr-x[-rw-r--r--]index.php107
-rw-r--r--plugins/playvideos/README.md26
-rw-r--r--plugins/qrcode/qrcode.php2
-rw-r--r--shaarli_version.php2
-rw-r--r--tests/ApplicationUtilsTest.php228
-rw-r--r--tests/CacheTest.php17
-rw-r--r--tests/CachedPageTest.php2
-rw-r--r--tests/LinkDBTest.php25
-rw-r--r--tests/UtilsTest.php37
-rw-r--r--tests/plugins/PlugQrcodeTest.php4
-rw-r--r--tpl/daily.html2
-rw-r--r--tpl/linklist.html6
-rw-r--r--tpl/page.footer.html12
-rw-r--r--tpl/picwall.html2
19 files changed, 567 insertions, 89 deletions
diff --git a/.gitignore b/.gitignore
index b98c38b9..75cd3a6b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,9 +19,8 @@ composer.lock
19# Ignore development and test resources 19# Ignore development and test resources
20coverage 20coverage
21doxygen 21doxygen
22tests/datastore.php 22sandbox
23tests/dummycache/
24phpmd.html 23phpmd.html
25 24
26# Ignore user plugin configuration 25# Ignore user plugin configuration
27plugins/*/config.php \ No newline at end of file 26plugins/*/config.php
diff --git a/Makefile b/Makefile
index c560d8d1..a86f9aa8 100644
--- a/Makefile
+++ b/Makefile
@@ -110,6 +110,7 @@ test:
110 @echo "-------" 110 @echo "-------"
111 @echo "PHPUNIT" 111 @echo "PHPUNIT"
112 @echo "-------" 112 @echo "-------"
113 @mkdir -p sandbox
113 @$(BIN)/phpunit tests 114 @$(BIN)/phpunit tests
114 115
115## 116##
@@ -119,6 +120,7 @@ test:
119### remove all unversioned files 120### remove all unversioned files
120clean: 121clean:
121 @git clean -df 122 @git clean -df
123 @rm -rf sandbox
122 124
123### generate Doxygen documentation 125### generate Doxygen documentation
124doxygen: clean 126doxygen: clean
diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php
index b0e94e24..274331e1 100644
--- a/application/ApplicationUtils.php
+++ b/application/ApplicationUtils.php
@@ -4,6 +4,110 @@
4 */ 4 */
5class ApplicationUtils 5class ApplicationUtils
6{ 6{
7 private static $GIT_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli';
8 private static $GIT_BRANCHES = array('master', 'stable');
9 private static $VERSION_FILE = 'shaarli_version.php';
10 private static $VERSION_START_TAG = '<?php /* ';
11 private static $VERSION_END_TAG = ' */ ?>';
12
13 /**
14 * Gets the latest version code from the Git repository
15 *
16 * The code is read from the raw content of the version file on the Git server.
17 *
18 * @return mixed the version code from the repository if available, else 'false'
19 */
20 public static function getLatestGitVersionCode($url, $timeout=2)
21 {
22 list($headers, $data) = get_http_url($url, $timeout);
23
24 if (strpos($headers[0], '200 OK') === false) {
25 error_log('Failed to retrieve ' . $url);
26 return false;
27 }
28
29 return str_replace(
30 array(self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL),
31 array('', '', ''),
32 $data
33 );
34 }
35
36 /**
37 * Checks if a new Shaarli version has been published on the Git repository
38 *
39 * Updates checks are run periodically, according to the following criteria:
40 * - the update checks are enabled (install, global config);
41 * - the user is logged in (or this is an open instance);
42 * - the last check is older than a given interval;
43 * - the check is non-blocking if the HTTPS connection to Git fails;
44 * - in case of failure, the update file's modification date is updated,
45 * to avoid intempestive connection attempts.
46 *
47 * @param string $currentVersion the current version code
48 * @param string $updateFile the file where to store the latest version code
49 * @param int $checkInterval the minimum interval between update checks (in seconds
50 * @param bool $enableCheck whether to check for new versions
51 * @param bool $isLoggedIn whether the user is logged in
52 *
53 * @throws Exception an invalid branch has been set for update checks
54 *
55 * @return mixed the new version code if available and greater, else 'false'
56 */
57 public static function checkUpdate($currentVersion,
58 $updateFile,
59 $checkInterval,
60 $enableCheck,
61 $isLoggedIn,
62 $branch='stable')
63 {
64 if (! $isLoggedIn) {
65 // Do not check versions for visitors
66 return false;
67 }
68
69 if (empty($enableCheck)) {
70 // Do not check if the user doesn't want to
71 return false;
72 }
73
74 if (is_file($updateFile) && (filemtime($updateFile) > time() - $checkInterval)) {
75 // Shaarli has checked for updates recently - skip HTTP query
76 $latestKnownVersion = file_get_contents($updateFile);
77
78 if (version_compare($latestKnownVersion, $currentVersion) == 1) {
79 return $latestKnownVersion;
80 }
81 return false;
82 }
83
84 if (! in_array($branch, self::$GIT_BRANCHES)) {
85 throw new Exception(
86 'Invalid branch selected for updates: "' . $branch . '"'
87 );
88 }
89
90 // Late Static Binding allows overriding within tests
91 // See http://php.net/manual/en/language.oop5.late-static-bindings.php
92 $latestVersion = static::getLatestGitVersionCode(
93 self::$GIT_URL . '/' . $branch . '/' . self::$VERSION_FILE
94 );
95
96 if (! $latestVersion) {
97 // Only update the file's modification date
98 file_put_contents($updateFile, $currentVersion);
99 return false;
100 }
101
102 // Update the file's content and modification date
103 file_put_contents($updateFile, $latestVersion);
104
105 if (version_compare($latestVersion, $currentVersion) == 1) {
106 return $latestVersion;
107 }
108
109 return false;
110 }
7 111
8 /** 112 /**
9 * Checks the PHP version to ensure Shaarli can run 113 * Checks the PHP version to ensure Shaarli can run
diff --git a/application/LinkDB.php b/application/LinkDB.php
index 15fadbc3..f771ac8b 100644
--- a/application/LinkDB.php
+++ b/application/LinkDB.php
@@ -57,18 +57,25 @@ class LinkDB implements Iterator, Countable, ArrayAccess
57 // Hide public links 57 // Hide public links
58 private $_hidePublicLinks; 58 private $_hidePublicLinks;
59 59
60 // link redirector set in user settings.
61 private $_redirector;
62
60 /** 63 /**
61 * Creates a new LinkDB 64 * Creates a new LinkDB
62 * 65 *
63 * Checks if the datastore exists; else, attempts to create a dummy one. 66 * Checks if the datastore exists; else, attempts to create a dummy one.
64 * 67 *
65 * @param $isLoggedIn is the user logged in? 68 * @param string $datastore datastore file path.
69 * @param boolean $isLoggedIn is the user logged in?
70 * @param boolean $hidePublicLinks if true all links are private.
71 * @param string $redirector link redirector set in user settings.
66 */ 72 */
67 function __construct($datastore, $isLoggedIn, $hidePublicLinks) 73 function __construct($datastore, $isLoggedIn, $hidePublicLinks, $redirector = '')
68 { 74 {
69 $this->_datastore = $datastore; 75 $this->_datastore = $datastore;
70 $this->_loggedIn = $isLoggedIn; 76 $this->_loggedIn = $isLoggedIn;
71 $this->_hidePublicLinks = $hidePublicLinks; 77 $this->_hidePublicLinks = $hidePublicLinks;
78 $this->_redirector = $redirector;
72 $this->_checkDB(); 79 $this->_checkDB();
73 $this->_readDB(); 80 $this->_readDB();
74 } 81 }
@@ -259,7 +266,14 @@ You use the community supported version of the original Shaarli project, by Seba
259 266
260 // Escape links data 267 // Escape links data
261 foreach($this->_links as &$link) { 268 foreach($this->_links as &$link) {
262 sanitizeLink($link); 269 sanitizeLink($link);
270 // Do not use the redirector for internal links (Shaarli note URL starting with a '?').
271 if (!empty($this->_redirector) && !startsWith($link['url'], '?')) {
272 $link['real_url'] = $this->_redirector . urlencode($link['url']);
273 }
274 else {
275 $link['real_url'] = $link['url'];
276 }
263 } 277 }
264 } 278 }
265 279
diff --git a/application/Utils.php b/application/Utils.php
index b8579b48..f84f70e4 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -148,3 +148,56 @@ function is_session_id_valid($sessionId)
148 148
149 return true; 149 return true;
150} 150}
151
152/**
153 * In a string, converts URLs to clickable links.
154 *
155 * @param string $text input string.
156 * @param string $redirector if a redirector is set, use it to gerenate links.
157 *
158 * @return string returns $text with all links converted to HTML links.
159 *
160 * @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722
161 */
162function text2clickable($text, $redirector)
163{
164 $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[[:alnum:]]/?)!si';
165
166 if (empty($redirector)) {
167 return preg_replace($regex, '<a href="$1">$1</a>', $text);
168 }
169 // Redirector is set, urlencode the final URL.
170 return preg_replace_callback(
171 $regex,
172 function ($matches) use ($redirector) {
173 return '<a href="' . $redirector . urlencode($matches[1]) .'">'. $matches[1] .'</a>';
174 },
175 $text
176 );
177}
178
179/**
180 * This function inserts &nbsp; where relevant so that multiple spaces are properly displayed in HTML
181 * even in the absence of <pre> (This is used in description to keep text formatting).
182 *
183 * @param string $text input text.
184 *
185 * @return string formatted text.
186 */
187function space2nbsp($text)
188{
189 return preg_replace('/(^| ) /m', '$1&nbsp;', $text);
190}
191
192/**
193 * Format Shaarli's description
194 * TODO: Move me to ApplicationUtils when it's ready.
195 *
196 * @param string $description shaare's description.
197 * @param string $redirector if a redirector is set, use it to gerenate links.
198 *
199 * @return string formatted description.
200 */
201function format_description($description, $redirector) {
202 return nl2br(space2nbsp(text2clickable($description, $redirector)));
203}
diff --git a/index.php b/index.php
index b4d9395f..0dd5829b 100644..100755
--- a/index.php
+++ b/index.php
@@ -1,6 +1,6 @@
1<?php 1<?php
2/** 2/**
3 * Shaarli v0.6.0 - Shaare your links... 3 * Shaarli v0.6.1 - Shaare your links...
4 * 4 *
5 * The personal, minimalist, super-fast, no-database Delicious clone. 5 * The personal, minimalist, super-fast, no-database Delicious clone.
6 * 6 *
@@ -92,7 +92,8 @@ $GLOBALS['config']['ENABLE_THUMBNAILS'] = true;
92$GLOBALS['config']['ENABLE_LOCALCACHE'] = true; 92$GLOBALS['config']['ENABLE_LOCALCACHE'] = true;
93 93
94// Update check frequency for Shaarli. 86400 seconds=24 hours 94// Update check frequency for Shaarli. 86400 seconds=24 hours
95$GLOBALS['config']['UPDATECHECK_INTERVAL'] = 86400 ; 95$GLOBALS['config']['UPDATECHECK_BRANCH'] = 'stable';
96$GLOBALS['config']['UPDATECHECK_INTERVAL'] = 86400;
96 97
97 98
98/* 99/*
@@ -118,7 +119,7 @@ $GLOBALS['config']['PUBSUBHUB_URL'] = '';
118/* 119/*
119 * PHP configuration 120 * PHP configuration
120 */ 121 */
121define('shaarli_version', '0.6.0'); 122define('shaarli_version', '0.6.1');
122 123
123// http://server.com/x/shaarli --> /shaarli/ 124// http://server.com/x/shaarli --> /shaarli/
124define('WEB_PATH', substr($_SERVER["REQUEST_URI"], 0, 1+strrpos($_SERVER["REQUEST_URI"], '/', 0))); 125define('WEB_PATH', substr($_SERVER["REQUEST_URI"], 0, 1+strrpos($_SERVER["REQUEST_URI"], '/', 0)));
@@ -305,56 +306,15 @@ function setup_login_state() {
305} 306}
306$userIsLoggedIn = setup_login_state(); 307$userIsLoggedIn = setup_login_state();
307 308
308// Checks if an update is available for Shaarli.
309// (at most once a day, and only for registered user.)
310// Output: '' = no new version.
311// other= the available version.
312function checkUpdate()
313{
314 if (!isLoggedIn()) return ''; // Do not check versions for visitors.
315 if (empty($GLOBALS['config']['ENABLE_UPDATECHECK'])) return ''; // Do not check if the user doesn't want to.
316
317 // Get latest version number at most once a day.
318 if (!is_file($GLOBALS['config']['UPDATECHECK_FILENAME']) || (filemtime($GLOBALS['config']['UPDATECHECK_FILENAME'])<time()-($GLOBALS['config']['UPDATECHECK_INTERVAL'])))
319 {
320 $version = shaarli_version;
321 list($headers, $data) = get_http_url('https://raw.githubusercontent.com/shaarli/Shaarli/master/shaarli_version.php', 2);
322 if (strpos($headers[0], '200 OK') !== false) {
323 $version = str_replace(' */ ?>', '', str_replace('<?php /* ', '', $data));
324 }
325 // If failed, never mind. We don't want to bother the user with that.
326 file_put_contents($GLOBALS['config']['UPDATECHECK_FILENAME'],$version); // touch file date
327 }
328 // Compare versions:
329 $newestversion=file_get_contents($GLOBALS['config']['UPDATECHECK_FILENAME']);
330 if (version_compare($newestversion,shaarli_version)==1) return $newestversion;
331 return '';
332}
333
334 309
335// ----------------------------------------------------------------------------------------------- 310// -----------------------------------------------------------------------------------------------
336// Log to text file 311// Log to text file
337function logm($message) 312function logm($message)
338{ 313{
339 $t = strval(date('Y/m/d_H:i:s')).' - '.$_SERVER["REMOTE_ADDR"].' - '.strval($message)."\n"; 314 $t = strval(date('Y/m/d_H:i:s')).' - '.$_SERVER["REMOTE_ADDR"].' - '.strval($message)."\n";
340 file_put_contents($GLOBAL['config']['LOG_FILE'], $t, FILE_APPEND); 315 file_put_contents($GLOBALS['config']['LOG_FILE'], $t, FILE_APPEND);
341}
342
343// In a string, converts URLs to clickable links.
344// Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722
345function text2clickable($url)
346{
347 $redir = empty($GLOBALS['redirector']) ? '' : $GLOBALS['redirector'];
348 return preg_replace('!(((?:https?|ftp|file)://|apt:|magnet:)\S+[[:alnum:]]/?)!si','<a href="'.$redir.'$1" rel="nofollow">$1</a>',$url);
349} 316}
350 317
351// This function inserts &nbsp; where relevant so that multiple spaces are properly displayed in HTML
352// even in the absence of <pre> (This is used in description to keep text formatting)
353function keepMultipleSpaces($text)
354{
355 return str_replace(' ',' &nbsp;',$text);
356
357}
358// ------------------------------------------------------------------------------------------ 318// ------------------------------------------------------------------------------------------
359// Sniff browser language to display dates in the right format automatically. 319// Sniff browser language to display dates in the right format automatically.
360// (Note that is may not work on your server if the corresponding local is not installed.) 320// (Note that is may not work on your server if the corresponding local is not installed.)
@@ -672,7 +632,25 @@ class pageBuilder
672 private function initialize() 632 private function initialize()
673 { 633 {
674 $this->tpl = new RainTPL; 634 $this->tpl = new RainTPL;
675 $this->tpl->assign('newversion', escape(checkUpdate())); 635
636 try {
637 $version = ApplicationUtils::checkUpdate(
638 shaarli_version,
639 $GLOBALS['config']['UPDATECHECK_FILENAME'],
640 $GLOBALS['config']['UPDATECHECK_INTERVAL'],
641 $GLOBALS['config']['ENABLE_UPDATECHECK'],
642 isLoggedIn(),
643 $GLOBALS['config']['UPDATECHECK_BRANCH']
644 );
645 $this->tpl->assign('newVersion', escape($version));
646 $this->tpl->assign('versionError', '');
647
648 } catch (Exception $exc) {
649 logm($exc->getMessage());
650 $this->tpl->assign('newVersion', '');
651 $this->tpl->assign('versionError', escape($exc->getMessage()));
652 }
653
676 $this->tpl->assign('feedurl', escape(index_url($_SERVER))); 654 $this->tpl->assign('feedurl', escape(index_url($_SERVER)));
677 $searchcrits = ''; // Search criteria 655 $searchcrits = ''; // Search criteria
678 if (!empty($_GET['searchtags'])) { 656 if (!empty($_GET['searchtags'])) {
@@ -746,7 +724,8 @@ function showRSS()
746 $LINKSDB = new LinkDB( 724 $LINKSDB = new LinkDB(
747 $GLOBALS['config']['DATASTORE'], 725 $GLOBALS['config']['DATASTORE'],
748 isLoggedIn(), 726 isLoggedIn(),
749 $GLOBALS['config']['HIDE_PUBLIC_LINKS'] 727 $GLOBALS['config']['HIDE_PUBLIC_LINKS'],
728 $GLOBALS['redirector']
750 ); 729 );
751 // Read links from database (and filter private links if user it not logged in). 730 // Read links from database (and filter private links if user it not logged in).
752 731
@@ -797,7 +776,9 @@ function showRSS()
797 // If user wants permalinks first, put the final link in description 776 // If user wants permalinks first, put the final link in description
798 if ($usepermalinks===true) $descriptionlink = '(<a href="'.$absurl.'">Link</a>)'; 777 if ($usepermalinks===true) $descriptionlink = '(<a href="'.$absurl.'">Link</a>)';
799 if (strlen($link['description'])>0) $descriptionlink = '<br>'.$descriptionlink; 778 if (strlen($link['description'])>0) $descriptionlink = '<br>'.$descriptionlink;
800 echo '<description><![CDATA['.nl2br(keepMultipleSpaces(text2clickable($link['description']))).$descriptionlink.']]></description>'."\n</item>\n"; 779 echo '<description><![CDATA['.
780 format_description($link['description'], $GLOBALS['redirector']) .
781 $descriptionlink . ']]></description>' . "\n</item>\n";
801 $i++; 782 $i++;
802 } 783 }
803 echo '</channel></rss><!-- Cached version of '.escape(page_url($_SERVER)).' -->'; 784 echo '</channel></rss><!-- Cached version of '.escape(page_url($_SERVER)).' -->';
@@ -835,7 +816,8 @@ function showATOM()
835 $LINKSDB = new LinkDB( 816 $LINKSDB = new LinkDB(
836 $GLOBALS['config']['DATASTORE'], 817 $GLOBALS['config']['DATASTORE'],
837 isLoggedIn(), 818 isLoggedIn(),
838 $GLOBALS['config']['HIDE_PUBLIC_LINKS'] 819 $GLOBALS['config']['HIDE_PUBLIC_LINKS'],
820 $GLOBALS['redirector']
839 ); 821 );
840 822
841 // Optionally filter the results: 823 // Optionally filter the results:
@@ -876,7 +858,9 @@ function showATOM()
876 if ($usepermalinks===true) $descriptionlink = '(<a href="'.$absurl.'">Link</a>)'; 858 if ($usepermalinks===true) $descriptionlink = '(<a href="'.$absurl.'">Link</a>)';
877 if (strlen($link['description'])>0) $descriptionlink = '<br>'.$descriptionlink; 859 if (strlen($link['description'])>0) $descriptionlink = '<br>'.$descriptionlink;
878 860
879 $entries.='<content type="html"><![CDATA['.nl2br(keepMultipleSpaces(text2clickable($link['description']))).$descriptionlink."]]></content>\n"; 861 $entries .= '<content type="html"><![CDATA['.
862 format_description($link['description'], $GLOBALS['redirector']) .
863 $descriptionlink . "]]></content>\n";
880 if ($link['tags']!='') // Adding tags to each ATOM entry (as mentioned in ATOM specification) 864 if ($link['tags']!='') // Adding tags to each ATOM entry (as mentioned in ATOM specification)
881 { 865 {
882 foreach(explode(' ',$link['tags']) as $tag) 866 foreach(explode(' ',$link['tags']) as $tag)
@@ -929,7 +913,8 @@ function showDailyRSS() {
929 $LINKSDB = new LinkDB( 913 $LINKSDB = new LinkDB(
930 $GLOBALS['config']['DATASTORE'], 914 $GLOBALS['config']['DATASTORE'],
931 isLoggedIn(), 915 isLoggedIn(),
932 $GLOBALS['config']['HIDE_PUBLIC_LINKS'] 916 $GLOBALS['config']['HIDE_PUBLIC_LINKS'],
917 $GLOBALS['redirector']
933 ); 918 );
934 919
935 /* Some Shaarlies may have very few links, so we need to look 920 /* Some Shaarlies may have very few links, so we need to look
@@ -983,7 +968,7 @@ function showDailyRSS() {
983 // We pre-format some fields for proper output. 968 // We pre-format some fields for proper output.
984 foreach ($linkdates as $linkdate) { 969 foreach ($linkdates as $linkdate) {
985 $l = $LINKSDB[$linkdate]; 970 $l = $LINKSDB[$linkdate];
986 $l['formatedDescription'] = nl2br(keepMultipleSpaces(text2clickable($l['description']))); 971 $l['formatedDescription'] = format_description($l['description'], $GLOBALS['redirector']);
987 $l['thumbnail'] = thumbnail($l['url']); 972 $l['thumbnail'] = thumbnail($l['url']);
988 $l['timestamp'] = linkdate2timestamp($l['linkdate']); 973 $l['timestamp'] = linkdate2timestamp($l['linkdate']);
989 if (startsWith($l['url'], '?')) { 974 if (startsWith($l['url'], '?')) {
@@ -1016,7 +1001,8 @@ function showDaily()
1016 $LINKSDB = new LinkDB( 1001 $LINKSDB = new LinkDB(
1017 $GLOBALS['config']['DATASTORE'], 1002 $GLOBALS['config']['DATASTORE'],
1018 isLoggedIn(), 1003 isLoggedIn(),
1019 $GLOBALS['config']['HIDE_PUBLIC_LINKS'] 1004 $GLOBALS['config']['HIDE_PUBLIC_LINKS'],
1005 $GLOBALS['redirector']
1020 ); 1006 );
1021 1007
1022 $day=Date('Ymd',strtotime('-1 day')); // Yesterday, in format YYYYMMDD. 1008 $day=Date('Ymd',strtotime('-1 day')); // Yesterday, in format YYYYMMDD.
@@ -1047,7 +1033,7 @@ function showDaily()
1047 $taglist = explode(' ',$link['tags']); 1033 $taglist = explode(' ',$link['tags']);
1048 uasort($taglist, 'strcasecmp'); 1034 uasort($taglist, 'strcasecmp');
1049 $linksToDisplay[$key]['taglist']=$taglist; 1035 $linksToDisplay[$key]['taglist']=$taglist;
1050 $linksToDisplay[$key]['formatedDescription']=nl2br(keepMultipleSpaces(text2clickable($link['description']))); 1036 $linksToDisplay[$key]['formatedDescription'] = format_description($link['description'], $GLOBALS['redirector']);
1051 $linksToDisplay[$key]['thumbnail'] = thumbnail($link['url']); 1037 $linksToDisplay[$key]['thumbnail'] = thumbnail($link['url']);
1052 $linksToDisplay[$key]['timestamp'] = linkdate2timestamp($link['linkdate']); 1038 $linksToDisplay[$key]['timestamp'] = linkdate2timestamp($link['linkdate']);
1053 } 1039 }
@@ -1107,7 +1093,8 @@ function renderPage()
1107 $LINKSDB = new LinkDB( 1093 $LINKSDB = new LinkDB(
1108 $GLOBALS['config']['DATASTORE'], 1094 $GLOBALS['config']['DATASTORE'],
1109 isLoggedIn(), 1095 isLoggedIn(),
1110 $GLOBALS['config']['HIDE_PUBLIC_LINKS'] 1096 $GLOBALS['config']['HIDE_PUBLIC_LINKS'],
1097 $GLOBALS['redirector']
1111 ); 1098 );
1112 1099
1113 $PAGE = new pageBuilder; 1100 $PAGE = new pageBuilder;
@@ -1781,7 +1768,8 @@ function importFile()
1781 $LINKSDB = new LinkDB( 1768 $LINKSDB = new LinkDB(
1782 $GLOBALS['config']['DATASTORE'], 1769 $GLOBALS['config']['DATASTORE'],
1783 isLoggedIn(), 1770 isLoggedIn(),
1784 $GLOBALS['config']['HIDE_PUBLIC_LINKS'] 1771 $GLOBALS['config']['HIDE_PUBLIC_LINKS'],
1772 $GLOBALS['redirector']
1785 ); 1773 );
1786 $filename=$_FILES['filetoupload']['name']; 1774 $filename=$_FILES['filetoupload']['name'];
1787 $filesize=$_FILES['filetoupload']['size']; 1775 $filesize=$_FILES['filetoupload']['size'];
@@ -1932,8 +1920,7 @@ function buildLinkList($PAGE,$LINKSDB)
1932 while ($i<$end && $i<count($keys)) 1920 while ($i<$end && $i<count($keys))
1933 { 1921 {
1934 $link = $linksToDisplay[$keys[$i]]; 1922 $link = $linksToDisplay[$keys[$i]];
1935 $link['description']=nl2br(keepMultipleSpaces(text2clickable($link['description']))); 1923 $link['description'] = format_description($link['description'], $GLOBALS['redirector']);
1936 $title=$link['title'];
1937 $classLi = $i%2!=0 ? '' : 'publicLinkHightLight'; 1924 $classLi = $i%2!=0 ? '' : 'publicLinkHightLight';
1938 $link['class'] = ($link['private']==0 ? $classLi : 'private'); 1925 $link['class'] = ($link['private']==0 ? $classLi : 'private');
1939 $link['timestamp']=linkdate2timestamp($link['linkdate']); 1926 $link['timestamp']=linkdate2timestamp($link['linkdate']);
@@ -1974,6 +1961,10 @@ function buildLinkList($PAGE,$LINKSDB)
1974 'links' => $linkDisp, 1961 'links' => $linkDisp,
1975 'tags' => $LINKSDB->allTags(), 1962 'tags' => $LINKSDB->allTags(),
1976 ); 1963 );
1964 // FIXME! temporary fix - see #399.
1965 if (!empty($GLOBALS['pagetitle']) && count($linkDisp) == 1) {
1966 $data['pagetitle'] = $GLOBALS['pagetitle'];
1967 }
1977 1968
1978 $pluginManager = PluginManager::getInstance(); 1969 $pluginManager = PluginManager::getInstance();
1979 $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => isLoggedIn())); 1970 $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => isLoggedIn()));
diff --git a/plugins/playvideos/README.md b/plugins/playvideos/README.md
index ec1ead8d..b1698470 100644
--- a/plugins/playvideos/README.md
+++ b/plugins/playvideos/README.md
@@ -1,21 +1,29 @@
1### â–º Play Videos plugin for Shaarli 1### â–º Play Videos plugin for Shaarli
2This plugin adds a `â–º Play Videos` button to [Shaarli](https://github.com/shaarli/Shaarli)'s toolbar. Click this button to play all videos on the page in an overlay HTML5 player. Nice for continuous stream of music, documentaries, talks...
3 2
4This uses code from https://zaius.github.io/youtube_playlist/ and is currently only compatible with Youtube videos. 3Adds a `â–º Play Videos` button to [Shaarli](https://github.com/shaarli/Shaarli)'s toolbar. Click this button to play all videos on the page in an overlay HTML5 player. Nice for continuous stream of music, documentaries, talks...
4
5<!-- TODO screenshot -->
5 6
6![](https://cdn.mediacru.sh/D_izf0zjAtxy.png) 7This uses code from https://zaius.github.io/youtube_playlist/ and is currently only compatible with Youtube videos.
7 8
8#### Installation and setup 9#### Installation and setup
9Place the files in the `tpl/plugins/playvideos/` directory of your Shaarli.
10This is a default Shaarli plugin, you just have to enable it.
11 10
12To enable the plugin, add `playvideos` to the `TOOLBAR_PLUGINS` config option in your `index.php` or `data/options.php`. Example: 11This is a default Shaarli plugin, you just have to enable it. See https://github.com/shaarli/Shaarli/wiki/Shaarli-configuration/
13 12
14 $GLOBALS['config']['TOOLBAR_PLUGINS'] = array('aplugins', 'anotherone', 'playvideos');
15 13
16#### Troubleshooting 14#### Troubleshooting
15
17If your server has [Content Security Policy](http://content-security-policy.com/) headers enabled, this may prevent the script from loading fully. You should relax the CSP in your server settings. Example CSP rule for apache2: 16If your server has [Content Security Policy](http://content-security-policy.com/) headers enabled, this may prevent the script from loading fully. You should relax the CSP in your server settings. Example CSP rule for apache2:
18`Header set Content-Security-Policy "script-src 'self' 'unsafe-inline' https://www.youtube.com https://s.ytimg.com 'unsafe-eval'"` 17
18In `/etc/apache2/conf-available/shaarli-csp.conf`:
19
20```apache
21<Directory /path/to/shaarli>
22 Header set Content-Security-Policy "script-src 'self' 'unsafe-inline' https://www.youtube.com https://s.ytimg.com 'unsafe-eval'"
23</Directory>
24```
25
26Then run `a2enconf shaarli-csp; service apache2 reload`
19 27
20### License 28### License
21``` 29```
@@ -68,4 +76,4 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
68THE SOFTWARE. 76THE SOFTWARE.
69 77
70---------------------------------------------------- 78----------------------------------------------------
71``` \ No newline at end of file 79```
diff --git a/plugins/qrcode/qrcode.php b/plugins/qrcode/qrcode.php
index 1080c964..5f6e76a2 100644
--- a/plugins/qrcode/qrcode.php
+++ b/plugins/qrcode/qrcode.php
@@ -17,7 +17,7 @@ function hook_qrcode_render_linklist($data)
17 $qrcode_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.html'); 17 $qrcode_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/qrcode/qrcode.html');
18 18
19 foreach ($data['links'] as &$value) { 19 foreach ($data['links'] as &$value) {
20 $qrcode = sprintf($qrcode_html, $value['url'], $value['url'], PluginManager::$PLUGINS_PATH); 20 $qrcode = sprintf($qrcode_html, $value['real_url'], $value['real_url'], PluginManager::$PLUGINS_PATH);
21 $value['link_plugin'][] = $qrcode; 21 $value['link_plugin'][] = $qrcode;
22 } 22 }
23 23
diff --git a/shaarli_version.php b/shaarli_version.php
index a5f0a095..11ad87d7 100644
--- a/shaarli_version.php
+++ b/shaarli_version.php
@@ -1 +1 @@
<?php /* 0.6.0 */ ?> <?php /* 0.6.1 */ ?>
diff --git a/tests/ApplicationUtilsTest.php b/tests/ApplicationUtilsTest.php
index 01301e68..6064357d 100644
--- a/tests/ApplicationUtilsTest.php
+++ b/tests/ApplicationUtilsTest.php
@@ -5,12 +5,240 @@
5 5
6require_once 'application/ApplicationUtils.php'; 6require_once 'application/ApplicationUtils.php';
7 7
8/**
9 * Fake ApplicationUtils class to avoid HTTP requests
10 */
11class FakeApplicationUtils extends ApplicationUtils
12{
13 public static $VERSION_CODE = '';
14
15 /**
16 * Toggle HTTP requests, allow overriding the version code
17 */
18 public static function getLatestGitVersionCode($url, $timeout=0)
19 {
20 return self::$VERSION_CODE;
21 }
22}
23
8 24
9/** 25/**
10 * Unitary tests for Shaarli utilities 26 * Unitary tests for Shaarli utilities
11 */ 27 */
12class ApplicationUtilsTest extends PHPUnit_Framework_TestCase 28class ApplicationUtilsTest extends PHPUnit_Framework_TestCase
13{ 29{
30 protected static $testUpdateFile = 'sandbox/update.txt';
31 protected static $testVersion = '0.5.0';
32 protected static $versionPattern = '/^\d+\.\d+\.\d+$/';
33
34 /**
35 * Reset test data for each test
36 */
37 public function setUp()
38 {
39 FakeApplicationUtils::$VERSION_CODE = '';
40 if (file_exists(self::$testUpdateFile)) {
41 unlink(self::$testUpdateFile);
42 }
43 }
44
45 /**
46 * Retrieve the latest version code available on Git
47 *
48 * Expected format: Semantic Versioning - major.minor.patch
49 */
50 public function testGetLatestGitVersionCode()
51 {
52 $testTimeout = 10;
53
54 $this->assertEquals(
55 '0.5.4',
56 ApplicationUtils::getLatestGitVersionCode(
57 'https://raw.githubusercontent.com/shaarli/Shaarli/'
58 .'v0.5.4/shaarli_version.php',
59 $testTimeout
60 )
61 );
62 $this->assertRegexp(
63 self::$versionPattern,
64 ApplicationUtils::getLatestGitVersionCode(
65 'https://raw.githubusercontent.com/shaarli/Shaarli/'
66 .'master/shaarli_version.php',
67 $testTimeout
68 )
69 );
70 }
71
72 /**
73 * Attempt to retrieve the latest version from an invalid URL
74 */
75 public function testGetLatestGitVersionCodeInvalidUrl()
76 {
77 $this->assertFalse(
78 ApplicationUtils::getLatestGitVersionCode('htttp://null.io', 1)
79 );
80 }
81
82 /**
83 * Test update checks - the user is logged off
84 */
85 public function testCheckUpdateLoggedOff()
86 {
87 $this->assertFalse(
88 ApplicationUtils::checkUpdate(self::$testVersion, 'null', 0, false, false)
89 );
90 }
91
92 /**
93 * Test update checks - the user has disabled updates
94 */
95 public function testCheckUpdateUserDisabled()
96 {
97 $this->assertFalse(
98 ApplicationUtils::checkUpdate(self::$testVersion, 'null', 0, false, true)
99 );
100 }
101
102 /**
103 * A newer version is available
104 */
105 public function testCheckUpdateNewVersionAvailable()
106 {
107 $newVersion = '1.8.3';
108 FakeApplicationUtils::$VERSION_CODE = $newVersion;
109
110 $version = FakeApplicationUtils::checkUpdate(
111 self::$testVersion,
112 self::$testUpdateFile,
113 100,
114 true,
115 true
116 );
117
118 $this->assertEquals($newVersion, $version);
119 }
120
121 /**
122 * No available information about versions
123 */
124 public function testCheckUpdateNewVersionUnavailable()
125 {
126 $version = FakeApplicationUtils::checkUpdate(
127 self::$testVersion,
128 self::$testUpdateFile,
129 100,
130 true,
131 true
132 );
133
134 $this->assertFalse($version);
135 }
136
137 /**
138 * Test update checks - invalid Git branch
139 * @expectedException Exception
140 * @expectedExceptionMessageRegExp /Invalid branch selected for updates/
141 */
142 public function testCheckUpdateInvalidGitBranch()
143 {
144 ApplicationUtils::checkUpdate('', 'null', 0, true, true, 'unstable');
145 }
146
147 /**
148 * Shaarli is up-to-date
149 */
150 public function testCheckUpdateNewVersionUpToDate()
151 {
152 FakeApplicationUtils::$VERSION_CODE = self::$testVersion;
153
154 $version = FakeApplicationUtils::checkUpdate(
155 self::$testVersion,
156 self::$testUpdateFile,
157 100,
158 true,
159 true
160 );
161
162 $this->assertFalse($version);
163 }
164
165 /**
166 * Time-traveller's Shaarli
167 */
168 public function testCheckUpdateNewVersionMaartiMcFly()
169 {
170 FakeApplicationUtils::$VERSION_CODE = '0.4.1';
171
172 $version = FakeApplicationUtils::checkUpdate(
173 self::$testVersion,
174 self::$testUpdateFile,
175 100,
176 true,
177 true
178 );
179
180 $this->assertFalse($version);
181 }
182
183 /**
184 * The version has been checked recently and Shaarli is up-to-date
185 */
186 public function testCheckUpdateNewVersionTwiceUpToDate()
187 {
188 FakeApplicationUtils::$VERSION_CODE = self::$testVersion;
189
190 // Create the update file
191 $version = FakeApplicationUtils::checkUpdate(
192 self::$testVersion,
193 self::$testUpdateFile,
194 100,
195 true,
196 true
197 );
198
199 $this->assertFalse($version);
200
201 // Reuse the update file
202 $version = FakeApplicationUtils::checkUpdate(
203 self::$testVersion,
204 self::$testUpdateFile,
205 100,
206 true,
207 true
208 );
209
210 $this->assertFalse($version);
211 }
212
213 /**
214 * The version has been checked recently and Shaarli is outdated
215 */
216 public function testCheckUpdateNewVersionTwiceOutdated()
217 {
218 $newVersion = '1.8.3';
219 FakeApplicationUtils::$VERSION_CODE = $newVersion;
220
221 // Create the update file
222 $version = FakeApplicationUtils::checkUpdate(
223 self::$testVersion,
224 self::$testUpdateFile,
225 100,
226 true,
227 true
228 );
229 $this->assertEquals($newVersion, $version);
230
231 // Reuse the update file
232 $version = FakeApplicationUtils::checkUpdate(
233 self::$testVersion,
234 self::$testUpdateFile,
235 100,
236 true,
237 true
238 );
239 $this->assertEquals($newVersion, $version);
240 }
241
14 /** 242 /**
15 * Check supported PHP versions 243 * Check supported PHP versions
16 */ 244 */
diff --git a/tests/CacheTest.php b/tests/CacheTest.php
index aa5395b0..26c43225 100644
--- a/tests/CacheTest.php
+++ b/tests/CacheTest.php
@@ -11,10 +11,10 @@ require_once 'application/Cache.php';
11/** 11/**
12 * Unitary tests for cached pages 12 * Unitary tests for cached pages
13 */ 13 */
14class CachedTest extends PHPUnit_Framework_TestCase 14class CacheTest extends PHPUnit_Framework_TestCase
15{ 15{
16 // test cache directory 16 // test cache directory
17 protected static $testCacheDir = 'tests/dummycache'; 17 protected static $testCacheDir = 'sandbox/dummycache';
18 18
19 // dummy cached file names / content 19 // dummy cached file names / content
20 protected static $pages = array('a', 'toto', 'd7b59c'); 20 protected static $pages = array('a', 'toto', 'd7b59c');
@@ -30,7 +30,7 @@ class CachedTest extends PHPUnit_Framework_TestCase
30 } else { 30 } else {
31 array_map('unlink', glob(self::$testCacheDir.'/*')); 31 array_map('unlink', glob(self::$testCacheDir.'/*'));
32 } 32 }
33 33
34 foreach (self::$pages as $page) { 34 foreach (self::$pages as $page) {
35 file_put_contents(self::$testCacheDir.'/'.$page.'.cache', $page); 35 file_put_contents(self::$testCacheDir.'/'.$page.'.cache', $page);
36 } 36 }
@@ -38,6 +38,15 @@ class CachedTest extends PHPUnit_Framework_TestCase
38 } 38 }
39 39
40 /** 40 /**
41 * Remove dummycache folder after each tests.
42 */
43 public function tearDown()
44 {
45 array_map('unlink', glob(self::$testCacheDir.'/*'));
46 rmdir(self::$testCacheDir);
47 }
48
49 /**
41 * Purge cached pages 50 * Purge cached pages
42 */ 51 */
43 public function testPurgeCachedPages() 52 public function testPurgeCachedPages()
@@ -56,7 +65,7 @@ class CachedTest extends PHPUnit_Framework_TestCase
56 public function testPurgeCachedPagesMissingDir() 65 public function testPurgeCachedPagesMissingDir()
57 { 66 {
58 $this->assertEquals( 67 $this->assertEquals(
59 'Cannot purge tests/dummycache_missing: no directory', 68 'Cannot purge sandbox/dummycache_missing: no directory',
60 purgeCachedPages(self::$testCacheDir.'_missing') 69 purgeCachedPages(self::$testCacheDir.'_missing')
61 ); 70 );
62 } 71 }
diff --git a/tests/CachedPageTest.php b/tests/CachedPageTest.php
index e97af030..51565cd6 100644
--- a/tests/CachedPageTest.php
+++ b/tests/CachedPageTest.php
@@ -11,7 +11,7 @@ require_once 'application/CachedPage.php';
11class CachedPageTest extends PHPUnit_Framework_TestCase 11class CachedPageTest extends PHPUnit_Framework_TestCase
12{ 12{
13 // test cache directory 13 // test cache directory
14 protected static $testCacheDir = 'tests/pagecache'; 14 protected static $testCacheDir = 'sandbox/pagecache';
15 protected static $url = 'http://shaar.li/?do=atom'; 15 protected static $url = 'http://shaar.li/?do=atom';
16 protected static $filename; 16 protected static $filename;
17 17
diff --git a/tests/LinkDBTest.php b/tests/LinkDBTest.php
index 8929713d..7b22b270 100644
--- a/tests/LinkDBTest.php
+++ b/tests/LinkDBTest.php
@@ -16,7 +16,7 @@ require_once 'tests/utils/ReferenceLinkDB.php';
16class LinkDBTest extends PHPUnit_Framework_TestCase 16class LinkDBTest extends PHPUnit_Framework_TestCase
17{ 17{
18 // datastore to test write operations 18 // datastore to test write operations
19 protected static $testDatastore = 'tests/datastore.php'; 19 protected static $testDatastore = 'sandbox/datastore.php';
20 protected static $refDB = null; 20 protected static $refDB = null;
21 protected static $publicLinkDB = null; 21 protected static $publicLinkDB = null;
22 protected static $privateLinkDB = null; 22 protected static $privateLinkDB = null;
@@ -511,4 +511,27 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
511 sizeof(self::$publicLinkDB->filterFullText('free software')) 511 sizeof(self::$publicLinkDB->filterFullText('free software'))
512 ); 512 );
513 } 513 }
514
515 /**
516 * Test real_url without redirector.
517 */
518 public function testLinkRealUrlWithoutRedirector()
519 {
520 $db = new LinkDB(self::$testDatastore, false, false);
521 foreach($db as $link) {
522 $this->assertEquals($link['url'], $link['real_url']);
523 }
524 }
525
526 /**
527 * Test real_url with redirector.
528 */
529 public function testLinkRealUrlWithRedirector()
530 {
531 $redirector = 'http://redirector.to?';
532 $db = new LinkDB(self::$testDatastore, false, false, $redirector);
533 foreach($db as $link) {
534 $this->assertStringStartsWith($redirector, $link['real_url']);
535 }
536 }
514} 537}
diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php
index 4847ea94..02eecda2 100644
--- a/tests/UtilsTest.php
+++ b/tests/UtilsTest.php
@@ -187,4 +187,41 @@ class UtilsTest extends PHPUnit_Framework_TestCase
187 is_session_id_valid('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=') 187 is_session_id_valid('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=')
188 ); 188 );
189 } 189 }
190
191 /**
192 * Test text2clickable without a redirector being set.
193 */
194 public function testText2clickableWithoutRedirector()
195 {
196 $text = 'stuff http://hello.there/is=someone#here otherstuff';
197 $expectedText = 'stuff <a href="http://hello.there/is=someone#here">http://hello.there/is=someone#here</a> otherstuff';
198 $processedText = text2clickable($text, '');
199 $this->assertEquals($expectedText, $processedText);
200 }
201
202 /**
203 * Test text2clickable a redirector set.
204 */
205 public function testText2clickableWithRedirector()
206 {
207 $text = 'stuff http://hello.there/is=someone#here otherstuff';
208 $redirector = 'http://redirector.to';
209 $expectedText = 'stuff <a href="'.
210 $redirector .
211 urlencode('http://hello.there/is=someone#here') .
212 '">http://hello.there/is=someone#here</a> otherstuff';
213 $processedText = text2clickable($text, $redirector);
214 $this->assertEquals($expectedText, $processedText);
215 }
216
217 /**
218 * Test testSpace2nbsp.
219 */
220 public function testSpace2nbsp()
221 {
222 $text = ' Are you thrilled by flags ?'. PHP_EOL .' Really?';
223 $expectedText = '&nbsp; Are you &nbsp; thrilled &nbsp;by flags &nbsp; ?'. PHP_EOL .'&nbsp;Really?';
224 $processedText = space2nbsp($text);
225 $this->assertEquals($expectedText, $processedText);
226 }
190} 227}
diff --git a/tests/plugins/PlugQrcodeTest.php b/tests/plugins/PlugQrcodeTest.php
index 86dc7f29..c749fa86 100644
--- a/tests/plugins/PlugQrcodeTest.php
+++ b/tests/plugins/PlugQrcodeTest.php
@@ -30,7 +30,7 @@ class PlugQrcodeTest extends PHPUnit_Framework_TestCase
30 'title' => $str, 30 'title' => $str,
31 'links' => array( 31 'links' => array(
32 array( 32 array(
33 'url' => $str, 33 'real_url' => $str,
34 ) 34 )
35 ) 35 )
36 ); 36 );
@@ -39,7 +39,7 @@ class PlugQrcodeTest extends PHPUnit_Framework_TestCase
39 $link = $data['links'][0]; 39 $link = $data['links'][0];
40 // data shouldn't be altered 40 // data shouldn't be altered
41 $this->assertEquals($str, $data['title']); 41 $this->assertEquals($str, $data['title']);
42 $this->assertEquals($str, $link['url']); 42 $this->assertEquals($str, $link['real_url']);
43 43
44 // plugin data 44 // plugin data
45 $this->assertEquals(1, count($link['link_plugin'])); 45 $this->assertEquals(1, count($link['link_plugin']));
diff --git a/tpl/daily.html b/tpl/daily.html
index 93a3ab45..063dc89a 100644
--- a/tpl/daily.html
+++ b/tpl/daily.html
@@ -66,7 +66,7 @@
66 </div> 66 </div>
67 {/if} 67 {/if}
68 <div class="dailyEntryTitle"> 68 <div class="dailyEntryTitle">
69 <a href="{$link.url}">{$link.title}</a> 69 <a href="{$link.real_url}">{$link.title}</a>
70 </div> 70 </div>
71 {if="$link.thumbnail"} 71 {if="$link.thumbnail"}
72 <div class="dailyEntryThumbnail">{$link.thumbnail}</div> 72 <div class="dailyEntryThumbnail">{$link.thumbnail}</div>
diff --git a/tpl/linklist.html b/tpl/linklist.html
index f6e9e82b..666748a7 100644
--- a/tpl/linklist.html
+++ b/tpl/linklist.html
@@ -70,7 +70,9 @@
70 </form> 70 </form>
71 </div> 71 </div>
72 {/if} 72 {/if}
73 <span class="linktitle"><a href="{$redirector}{$value.url}">{$value.title}</a></span> 73 <span class="linktitle">
74 <a href="{$value.real_url}">{$value.title}</a>
75 </span>
74 <br> 76 <br>
75 {if="$value.description"}<div class="linkdescription">{$value.description}</div>{/if} 77 {if="$value.description"}<div class="linkdescription">{$value.description}</div>{/if}
76 {if="!$GLOBALS['config']['HIDE_TIMESTAMPS'] || isLoggedIn()"} 78 {if="!$GLOBALS['config']['HIDE_TIMESTAMPS'] || isLoggedIn()"}
@@ -83,7 +85,7 @@
83 <span>{$value}</span> - 85 <span>{$value}</span> -
84 {/loop} 86 {/loop}
85 87
86 <a href="{$value.url}"><span class="linkurl" title="Short link">{$value.url}</span></a><br> 88 <a href="{$value.real_url}"><span class="linkurl" title="Short link">{$value.url}</span></a><br>
87 {if="$value.tags"} 89 {if="$value.tags"}
88 <div class="linktaglist"> 90 <div class="linktaglist">
89 {loop="value.taglist"}<span class="linktag" title="Add tag"><a href="?addtag={$value|urlencode}">{$value}</a></span> {/loop} 91 {loop="value.taglist"}<span class="linktag" title="Add tag"><a href="?addtag={$value|urlencode}">{$value}</a></span> {/loop}
diff --git a/tpl/page.footer.html b/tpl/page.footer.html
index 6c29850f..b20aae54 100644
--- a/tpl/page.footer.html
+++ b/tpl/page.footer.html
@@ -4,8 +4,16 @@
4 {$value} 4 {$value}
5 {/loop} 5 {/loop}
6</div> 6</div>
7{if="$newversion"} 7{if="$newVersion"}
8 <div id="newversion"><span id="version_id">&#x25CF;</span> Shaarli {$newversion} is <a href="https://github.com/shaarli/Shaarli/releases">available</a>.</div> 8<div id="newversion">
9 <span id="version_id">&#x25CF;</span> Shaarli {$newVersion} is
10 <a href="https://github.com/shaarli/Shaarli/releases">available</a>.
11</div>
12{/if}
13{if="$versionError"}
14<div id="newversion">
15 Error: {$versionError}
16</div>
9{/if} 17{/if}
10{if="isLoggedIn()"} 18{if="isLoggedIn()"}
11<script>function confirmDeleteLink() { var agree=confirm("Are you sure you want to delete this link ?"); if (agree) return true ; else return false ; }</script> 19<script>function confirmDeleteLink() { var agree=confirm("Are you sure you want to delete this link ?"); if (agree) return true ; else return false ; }</script>
diff --git a/tpl/picwall.html b/tpl/picwall.html
index 97d5efdf..230c948b 100644
--- a/tpl/picwall.html
+++ b/tpl/picwall.html
@@ -16,7 +16,7 @@
16 <div id="picwall_container"> 16 <div id="picwall_container">
17 {loop="linksToDisplay"} 17 {loop="linksToDisplay"}
18 <div class="picwall_pictureframe"> 18 <div class="picwall_pictureframe">
19 {$value.thumbnail}<a href="{$value.url}"><span class="info">{$value.title}</span></a> 19 {$value.thumbnail}<a href="{$value.real_url}"><span class="info">{$value.title}</span></a>
20 {loop="$value.picwall_plugin"} 20 {loop="$value.picwall_plugin"}
21 {$value} 21 {$value}
22 {/loop} 22 {/loop}