aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.gitattributes1
-rw-r--r--application/HttpUtils.php160
-rw-r--r--application/Languages.php21
-rw-r--r--application/LinkDB.php4
-rw-r--r--application/NetscapeBookmarkUtils.php141
-rw-r--r--application/PageBuilder.php1
-rw-r--r--application/PluginManager.php2
-rw-r--r--application/Router.php2
-rw-r--r--application/Updater.php4
-rw-r--r--composer.json8
-rw-r--r--index.php151
-rw-r--r--plugins/addlink_toolbar/addlink_toolbar.php2
-rw-r--r--plugins/playvideos/playvideos.php2
-rw-r--r--plugins/wallabag/README.md24
-rw-r--r--tests/LanguagesTest.php41
-rw-r--r--tests/LinkDBTest.php4
-rw-r--r--tests/NetscapeBookmarkUtils/BookmarkExportTest.php (renamed from tests/NetscapeBookmarkUtilsTest.php)4
-rw-r--r--tests/NetscapeBookmarkUtils/BookmarkImportTest.php546
-rw-r--r--tests/NetscapeBookmarkUtils/input/empty.htm0
-rw-r--r--tests/NetscapeBookmarkUtils/input/internet_explorer_encoding.htm9
-rw-r--r--tests/NetscapeBookmarkUtils/input/netscape_basic.htm11
-rw-r--r--tests/NetscapeBookmarkUtils/input/netscape_nested.htm31
-rw-r--r--tests/NetscapeBookmarkUtils/input/no_doctype.htm7
-rw-r--r--tests/NetscapeBookmarkUtils/input/same_date.htm11
-rw-r--r--tests/PluginManagerTest.php2
-rw-r--r--tests/Updater/UpdaterTest.php24
-rw-r--r--tests/utils/ReferenceLinkDB.php2
-rw-r--r--tests/utils/config/configInvalid.json.php2
-rw-r--r--tpl/import.html38
-rw-r--r--tpl/tools.html2
30 files changed, 1083 insertions, 174 deletions
diff --git a/.gitattributes b/.gitattributes
index aaf6a39e..d753b1db 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -21,7 +21,6 @@ Dockerfile text
21.gitattributes export-ignore 21.gitattributes export-ignore
22.gitignore export-ignore 22.gitignore export-ignore
23.travis.yml export-ignore 23.travis.yml export-ignore
24composer.json export-ignore
25doc/**/*.json export-ignore 24doc/**/*.json export-ignore
26doc/**/*.md export-ignore 25doc/**/*.md export-ignore
27docker/ export-ignore 26docker/ export-ignore
diff --git a/application/HttpUtils.php b/application/HttpUtils.php
index 2e0792f9..27a39d3d 100644
--- a/application/HttpUtils.php
+++ b/application/HttpUtils.php
@@ -1,6 +1,7 @@
1<?php 1<?php
2/** 2/**
3 * GET an HTTP URL to retrieve its content 3 * GET an HTTP URL to retrieve its content
4 * Uses the cURL library or a fallback method
4 * 5 *
5 * @param string $url URL to get (http://...) 6 * @param string $url URL to get (http://...)
6 * @param int $timeout network timeout (in seconds) 7 * @param int $timeout network timeout (in seconds)
@@ -20,38 +21,177 @@
20 * echo 'There was an error: '.htmlspecialchars($headers[0]); 21 * echo 'There was an error: '.htmlspecialchars($headers[0]);
21 * } 22 * }
22 * 23 *
23 * @see http://php.net/manual/en/function.file-get-contents.php 24 * @see https://secure.php.net/manual/en/ref.curl.php
24 * @see http://php.net/manual/en/function.stream-context-create.php 25 * @see https://secure.php.net/manual/en/functions.anonymous.php
25 * @see http://php.net/manual/en/function.get-headers.php 26 * @see https://secure.php.net/manual/en/function.preg-split.php
27 * @see https://secure.php.net/manual/en/function.explode.php
28 * @see http://stackoverflow.com/q/17641073
29 * @see http://stackoverflow.com/q/9183178
30 * @see http://stackoverflow.com/q/1462720
26 */ 31 */
27function get_http_response($url, $timeout = 30, $maxBytes = 4194304) 32function get_http_response($url, $timeout = 30, $maxBytes = 4194304)
28{ 33{
29 $urlObj = new Url($url); 34 $urlObj = new Url($url);
30 $cleanUrl = $urlObj->idnToAscii(); 35 $cleanUrl = $urlObj->idnToAscii();
31 36
32 if (! filter_var($cleanUrl, FILTER_VALIDATE_URL) || ! $urlObj->isHttp()) { 37 if (!filter_var($cleanUrl, FILTER_VALIDATE_URL) || !$urlObj->isHttp()) {
33 return array(array(0 => 'Invalid HTTP Url'), false); 38 return array(array(0 => 'Invalid HTTP Url'), false);
34 } 39 }
35 40
41 $userAgent =
42 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:45.0)'
43 . ' Gecko/20100101 Firefox/45.0';
44 $acceptLanguage =
45 substr(setlocale(LC_COLLATE, 0), 0, 2) . ',en-US;q=0.7,en;q=0.3';
46 $maxRedirs = 3;
47
48 if (!function_exists('curl_init')) {
49 return get_http_response_fallback(
50 $cleanUrl,
51 $timeout,
52 $maxBytes,
53 $userAgent,
54 $acceptLanguage,
55 $maxRedirs
56 );
57 }
58
59 $ch = curl_init($cleanUrl);
60 if ($ch === false) {
61 return array(array(0 => 'curl_init() error'), false);
62 }
63
64 // General cURL settings
65 curl_setopt($ch, CURLOPT_AUTOREFERER, true);
66 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
67 curl_setopt($ch, CURLOPT_HEADER, true);
68 curl_setopt(
69 $ch,
70 CURLOPT_HTTPHEADER,
71 array('Accept-Language: ' . $acceptLanguage)
72 );
73 curl_setopt($ch, CURLOPT_MAXREDIRS, $maxRedirs);
74 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
75 curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
76 curl_setopt($ch, CURLOPT_USERAGENT, $userAgent);
77
78 // Max download size management
79 curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024);
80 curl_setopt($ch, CURLOPT_NOPROGRESS, false);
81 curl_setopt($ch, CURLOPT_PROGRESSFUNCTION,
82 function($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes)
83 {
84 if (version_compare(phpversion(), '5.5', '<')) {
85 // PHP version lower than 5.5
86 // Callback has 4 arguments
87 $downloaded = $arg1;
88 } else {
89 // Callback has 5 arguments
90 $downloaded = $arg2;
91 }
92 // Non-zero return stops downloading
93 return ($downloaded > $maxBytes) ? 1 : 0;
94 }
95 );
96
97 $response = curl_exec($ch);
98 $errorNo = curl_errno($ch);
99 $errorStr = curl_error($ch);
100 $headSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
101 curl_close($ch);
102
103 if ($response === false) {
104 if ($errorNo == CURLE_COULDNT_RESOLVE_HOST) {
105 /*
106 * Workaround to match fallback method behaviour
107 * Removing this would require updating
108 * GetHttpUrlTest::testGetInvalidRemoteUrl()
109 */
110 return array(false, false);
111 }
112 return array(array(0 => 'curl_exec() error: ' . $errorStr), false);
113 }
114
115 // Formatting output like the fallback method
116 $rawHeaders = substr($response, 0, $headSize);
117
118 // Keep only headers from latest redirection
119 $rawHeadersArrayRedirs = explode("\r\n\r\n", trim($rawHeaders));
120 $rawHeadersLastRedir = end($rawHeadersArrayRedirs);
121
122 $content = substr($response, $headSize);
123 $headers = array();
124 foreach (preg_split('~[\r\n]+~', $rawHeadersLastRedir) as $line) {
125 if (empty($line) or ctype_space($line)) {
126 continue;
127 }
128 $splitLine = explode(': ', $line, 2);
129 if (count($splitLine) > 1) {
130 $key = $splitLine[0];
131 $value = $splitLine[1];
132 if (array_key_exists($key, $headers)) {
133 if (!is_array($headers[$key])) {
134 $headers[$key] = array(0 => $headers[$key]);
135 }
136 $headers[$key][] = $value;
137 } else {
138 $headers[$key] = $value;
139 }
140 } else {
141 $headers[] = $splitLine[0];
142 }
143 }
144
145 return array($headers, $content);
146}
147
148/**
149 * GET an HTTP URL to retrieve its content (fallback method)
150 *
151 * @param string $cleanUrl URL to get (http://... valid and in ASCII form)
152 * @param int $timeout network timeout (in seconds)
153 * @param int $maxBytes maximum downloaded bytes
154 * @param string $userAgent "User-Agent" header
155 * @param string $acceptLanguage "Accept-Language" header
156 * @param int $maxRedr maximum amount of redirections followed
157 *
158 * @return array HTTP response headers, downloaded content
159 *
160 * Output format:
161 * [0] = associative array containing HTTP response headers
162 * [1] = URL content (downloaded data)
163 *
164 * @see http://php.net/manual/en/function.file-get-contents.php
165 * @see http://php.net/manual/en/function.stream-context-create.php
166 * @see http://php.net/manual/en/function.get-headers.php
167 */
168function get_http_response_fallback(
169 $cleanUrl,
170 $timeout,
171 $maxBytes,
172 $userAgent,
173 $acceptLanguage,
174 $maxRedr
175) {
36 $options = array( 176 $options = array(
37 'http' => array( 177 'http' => array(
38 'method' => 'GET', 178 'method' => 'GET',
39 'timeout' => $timeout, 179 'timeout' => $timeout,
40 'user_agent' => 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:45.0)' 180 'user_agent' => $userAgent,
41 .' Gecko/20100101 Firefox/45.0', 181 'header' => "Accept: */*\r\n"
42 'accept_language' => substr(setlocale(LC_COLLATE, 0), 0, 2) . ',en-US;q=0.7,en;q=0.3', 182 . 'Accept-Language: ' . $acceptLanguage
43 ) 183 )
44 ); 184 );
45 185
46 stream_context_set_default($options); 186 stream_context_set_default($options);
47 list($headers, $finalUrl) = get_redirected_headers($cleanUrl); 187 list($headers, $finalUrl) = get_redirected_headers($cleanUrl, $maxRedr);
48 if (! $headers || strpos($headers[0], '200 OK') === false) { 188 if (! $headers || strpos($headers[0], '200 OK') === false) {
49 $options['http']['request_fulluri'] = true; 189 $options['http']['request_fulluri'] = true;
50 stream_context_set_default($options); 190 stream_context_set_default($options);
51 list($headers, $finalUrl) = get_redirected_headers($cleanUrl); 191 list($headers, $finalUrl) = get_redirected_headers($cleanUrl, $maxRedr);
52 } 192 }
53 193
54 if (! $headers || strpos($headers[0], '200 OK') === false) { 194 if (! $headers) {
55 return array($headers, false); 195 return array($headers, false);
56 } 196 }
57 197
diff --git a/application/Languages.php b/application/Languages.php
new file mode 100644
index 00000000..c8b0a25a
--- /dev/null
+++ b/application/Languages.php
@@ -0,0 +1,21 @@
1<?php
2
3/**
4 * Wrapper function for translation which match the API
5 * of gettext()/_() and ngettext().
6 *
7 * Not doing translation for now.
8 *
9 * @param string $text Text to translate.
10 * @param string $nText The plural message ID.
11 * @param int $nb The number of items for plural forms.
12 *
13 * @return String Text translated.
14 */
15function t($text, $nText = '', $nb = 0) {
16 if (empty($nText)) {
17 return $text;
18 }
19 $actualForm = $nb > 1 ? $nText : $text;
20 return sprintf($actualForm, $nb);
21}
diff --git a/application/LinkDB.php b/application/LinkDB.php
index 929a6b0f..d80434bf 100644
--- a/application/LinkDB.php
+++ b/application/LinkDB.php
@@ -291,7 +291,7 @@ You use the community supported version of the original Shaarli project, by Seba
291 291
292 // Remove private tags if the user is not logged in. 292 // Remove private tags if the user is not logged in.
293 if (! $this->_loggedIn) { 293 if (! $this->_loggedIn) {
294 $link['tags'] = preg_replace('/(^| )\.[^($| )]+/', '', $link['tags']); 294 $link['tags'] = preg_replace('/(^|\s+)\.[^($|\s)]+\s*/', ' ', $link['tags']);
295 } 295 }
296 296
297 // Do not use the redirector for internal links (Shaarli note URL starting with a '?'). 297 // Do not use the redirector for internal links (Shaarli note URL starting with a '?').
@@ -442,7 +442,7 @@ You use the community supported version of the original Shaarli project, by Seba
442 $tags = array(); 442 $tags = array();
443 $caseMapping = array(); 443 $caseMapping = array();
444 foreach ($this->_links as $link) { 444 foreach ($this->_links as $link) {
445 foreach (explode(' ', $link['tags']) as $tag) { 445 foreach (preg_split('/\s+/', $link['tags'], 0, PREG_SPLIT_NO_EMPTY) as $tag) {
446 if (empty($tag)) { 446 if (empty($tag)) {
447 continue; 447 continue;
448 } 448 }
diff --git a/application/NetscapeBookmarkUtils.php b/application/NetscapeBookmarkUtils.php
index fdbb0ad7..c3181254 100644
--- a/application/NetscapeBookmarkUtils.php
+++ b/application/NetscapeBookmarkUtils.php
@@ -51,4 +51,145 @@ class NetscapeBookmarkUtils
51 51
52 return $bookmarkLinks; 52 return $bookmarkLinks;
53 } 53 }
54
55 /**
56 * Generates an import status summary
57 *
58 * @param string $filename name of the file to import
59 * @param int $filesize size of the file to import
60 * @param int $importCount how many links were imported
61 * @param int $overwriteCount how many links were overwritten
62 * @param int $skipCount how many links were skipped
63 *
64 * @return string Summary of the bookmark import status
65 */
66 private static function importStatus(
67 $filename,
68 $filesize,
69 $importCount=0,
70 $overwriteCount=0,
71 $skipCount=0
72 )
73 {
74 $status = 'File '.$filename.' ('.$filesize.' bytes) ';
75 if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) {
76 $status .= 'has an unknown file format. Nothing was imported.';
77 } else {
78 $status .= 'was successfully processed: '.$importCount.' links imported, ';
79 $status .= $overwriteCount.' links overwritten, ';
80 $status .= $skipCount.' links skipped.';
81 }
82 return $status;
83 }
84
85 /**
86 * Imports Web bookmarks from an uploaded Netscape bookmark dump
87 *
88 * @param array $post Server $_POST parameters
89 * @param array $file Server $_FILES parameters
90 * @param LinkDB $linkDb Loaded LinkDB instance
91 * @param string $pagecache Page cache
92 *
93 * @return string Summary of the bookmark import status
94 */
95 public static function import($post, $files, $linkDb, $pagecache)
96 {
97 $filename = $files['filetoupload']['name'];
98 $filesize = $files['filetoupload']['size'];
99 $data = file_get_contents($files['filetoupload']['tmp_name']);
100
101 if (strpos($data, '<!DOCTYPE NETSCAPE-Bookmark-file-1>') === false) {
102 return self::importStatus($filename, $filesize);
103 }
104
105 // Overwrite existing links?
106 $overwrite = ! empty($post['overwrite']);
107
108 // Add tags to all imported links?
109 if (empty($post['default_tags'])) {
110 $defaultTags = array();
111 } else {
112 $defaultTags = preg_split(
113 '/[\s,]+/',
114 escape($post['default_tags'])
115 );
116 }
117
118 // links are imported as public by default
119 $defaultPrivacy = 0;
120
121 $parser = new NetscapeBookmarkParser(
122 true, // nested tag support
123 $defaultTags, // additional user-specified tags
124 strval(1 - $defaultPrivacy) // defaultPub = 1 - defaultPrivacy
125 );
126 $bookmarks = $parser->parseString($data);
127
128 $importCount = 0;
129 $overwriteCount = 0;
130 $skipCount = 0;
131
132 foreach ($bookmarks as $bkm) {
133 $private = $defaultPrivacy;
134 if (empty($post['privacy']) || $post['privacy'] == 'default') {
135 // use value from the imported file
136 $private = $bkm['pub'] == '1' ? 0 : 1;
137 } else if ($post['privacy'] == 'private') {
138 // all imported links are private
139 $private = 1;
140 } else if ($post['privacy'] == 'public') {
141 // all imported links are public
142 $private = 0;
143 }
144
145 $newLink = array(
146 'title' => $bkm['title'],
147 'url' => $bkm['uri'],
148 'description' => $bkm['note'],
149 'private' => $private,
150 'linkdate'=> '',
151 'tags' => $bkm['tags']
152 );
153
154 $existingLink = $linkDb->getLinkFromUrl($bkm['uri']);
155
156 if ($existingLink !== false) {
157 if ($overwrite === false) {
158 // Do not overwrite an existing link
159 $skipCount++;
160 continue;
161 }
162
163 // Overwrite an existing link, keep its date
164 $newLink['linkdate'] = $existingLink['linkdate'];
165 $linkDb[$existingLink['linkdate']] = $newLink;
166 $importCount++;
167 $overwriteCount++;
168 continue;
169 }
170
171 // Add a new link
172 $newLinkDate = new DateTime('@'.strval($bkm['time']));
173 while (!empty($linkDb[$newLinkDate->format(LinkDB::LINK_DATE_FORMAT)])) {
174 // Ensure the date/time is not already used
175 // - this hack is necessary as the date/time acts as a primary key
176 // - apply 1 second increments until an unused index is found
177 // See https://github.com/shaarli/Shaarli/issues/351
178 $newLinkDate->add(new DateInterval('PT1S'));
179 }
180 $linkDbDate = $newLinkDate->format(LinkDB::LINK_DATE_FORMAT);
181 $newLink['linkdate'] = $linkDbDate;
182 $linkDb[$linkDbDate] = $newLink;
183 $importCount++;
184 }
185
186 $linkDb->savedb($pagecache);
187 return self::importStatus(
188 $filename,
189 $filesize,
190 $importCount,
191 $overwriteCount,
192 $skipCount
193 );
194 }
54} 195}
diff --git a/application/PageBuilder.php b/application/PageBuilder.php
index 1ca0260a..42932f32 100644
--- a/application/PageBuilder.php
+++ b/application/PageBuilder.php
@@ -80,6 +80,7 @@ class PageBuilder
80 if (!empty($GLOBALS['plugin_errors'])) { 80 if (!empty($GLOBALS['plugin_errors'])) {
81 $this->tpl->assign('plugin_errors', $GLOBALS['plugin_errors']); 81 $this->tpl->assign('plugin_errors', $GLOBALS['plugin_errors']);
82 } 82 }
83 $this->tpl->assign('token', getToken($this->conf));
83 // To be removed with a proper theme configuration. 84 // To be removed with a proper theme configuration.
84 $this->tpl->assign('conf', $this->conf); 85 $this->tpl->assign('conf', $this->conf);
85 } 86 }
diff --git a/application/PluginManager.php b/application/PluginManager.php
index 07bc1da9..1e132a7f 100644
--- a/application/PluginManager.php
+++ b/application/PluginManager.php
@@ -214,4 +214,4 @@ class PluginFileNotFoundException extends Exception
214 { 214 {
215 $this->message = 'Plugin "'. $pluginName .'" files not found.'; 215 $this->message = 'Plugin "'. $pluginName .'" files not found.';
216 } 216 }
217} \ No newline at end of file 217}
diff --git a/application/Router.php b/application/Router.php
index 2c3934b0..caed4a28 100644
--- a/application/Router.php
+++ b/application/Router.php
@@ -138,4 +138,4 @@ class Router
138 138
139 return self::$PAGE_LINKLIST; 139 return self::$PAGE_LINKLIST;
140 } 140 }
141} \ No newline at end of file 141}
diff --git a/application/Updater.php b/application/Updater.php
index fd45d17f..b6cbc56c 100644
--- a/application/Updater.php
+++ b/application/Updater.php
@@ -198,11 +198,11 @@ class Updater
198 * Escape settings which have been manually escaped in every request in previous versions: 198 * Escape settings which have been manually escaped in every request in previous versions:
199 * - general.title 199 * - general.title
200 * - general.header_link 200 * - general.header_link
201 * - extras.redirector 201 * - redirector.url
202 * 202 *
203 * @return bool true if the update is successful, false otherwise. 203 * @return bool true if the update is successful, false otherwise.
204 */ 204 */
205 public function escapeUnescapedConfig() 205 public function updateMethodEscapeUnescapedConfig()
206 { 206 {
207 try { 207 try {
208 $this->conf->set('general.title', escape($this->conf->get('general.title'))); 208 $this->conf->set('general.title', escape($this->conf->get('general.title')));
diff --git a/composer.json b/composer.json
index dc1b509e..89a7e446 100644
--- a/composer.json
+++ b/composer.json
@@ -9,15 +9,9 @@
9 "wiki": "https://github.com/shaarli/Shaarli/wiki" 9 "wiki": "https://github.com/shaarli/Shaarli/wiki"
10 }, 10 },
11 "keywords": ["bookmark", "link", "share", "web"], 11 "keywords": ["bookmark", "link", "share", "web"],
12 "repositories": [
13 {
14 "type": "vcs",
15 "url": "https://github.com/shaarli/netscape-bookmark-parser"
16 }
17 ],
18 "require": { 12 "require": {
19 "php": ">=5.3.4", 13 "php": ">=5.3.4",
20 "kafene/netscape-bookmark-parser": "dev-shaarli-stable" 14 "shaarli/netscape-bookmark-parser": "1.*"
21 }, 15 },
22 "require-dev": { 16 "require-dev": {
23 "phpmd/phpmd" : "@stable", 17 "phpmd/phpmd" : "@stable",
diff --git a/index.php b/index.php
index 55b12adc..1f148d78 100644
--- a/index.php
+++ b/index.php
@@ -44,6 +44,10 @@ error_reporting(E_ALL^E_WARNING);
44//error_reporting(-1); 44//error_reporting(-1);
45 45
46 46
47// 3rd-party libraries
48require_once 'inc/rain.tpl.class.php';
49require_once __DIR__ . '/vendor/autoload.php';
50
47// Shaarli library 51// Shaarli library
48require_once 'application/ApplicationUtils.php'; 52require_once 'application/ApplicationUtils.php';
49require_once 'application/Cache.php'; 53require_once 'application/Cache.php';
@@ -53,6 +57,7 @@ require_once 'application/config/ConfigPlugin.php';
53require_once 'application/FeedBuilder.php'; 57require_once 'application/FeedBuilder.php';
54require_once 'application/FileUtils.php'; 58require_once 'application/FileUtils.php';
55require_once 'application/HttpUtils.php'; 59require_once 'application/HttpUtils.php';
60require_once 'application/Languages.php';
56require_once 'application/LinkDB.php'; 61require_once 'application/LinkDB.php';
57require_once 'application/LinkFilter.php'; 62require_once 'application/LinkFilter.php';
58require_once 'application/LinkUtils.php'; 63require_once 'application/LinkUtils.php';
@@ -64,7 +69,6 @@ require_once 'application/Utils.php';
64require_once 'application/PluginManager.php'; 69require_once 'application/PluginManager.php';
65require_once 'application/Router.php'; 70require_once 'application/Router.php';
66require_once 'application/Updater.php'; 71require_once 'application/Updater.php';
67require_once 'inc/rain.tpl.class.php';
68 72
69// Ensure the PHP version is supported 73// Ensure the PHP version is supported
70try { 74try {
@@ -783,8 +787,6 @@ function renderPage($conf, $pluginManager)
783 if ($targetPage == Router::$PAGE_LOGIN) 787 if ($targetPage == Router::$PAGE_LOGIN)
784 { 788 {
785 if ($conf->get('security.open_shaarli')) { header('Location: ?'); exit; } // No need to login for open Shaarli 789 if ($conf->get('security.open_shaarli')) { header('Location: ?'); exit; } // No need to login for open Shaarli
786 $token=''; if (ban_canLogin($conf)) $token=getToken($conf); // Do not waste token generation if not useful.
787 $PAGE->assign('token',$token);
788 if (isset($_GET['username'])) { 790 if (isset($_GET['username'])) {
789 $PAGE->assign('username', escape($_GET['username'])); 791 $PAGE->assign('username', escape($_GET['username']));
790 } 792 }
@@ -1105,7 +1107,6 @@ function renderPage($conf, $pluginManager)
1105 } 1107 }
1106 else // show the change password form. 1108 else // show the change password form.
1107 { 1109 {
1108 $PAGE->assign('token',getToken($conf));
1109 $PAGE->renderPage('changepassword'); 1110 $PAGE->renderPage('changepassword');
1110 exit; 1111 exit;
1111 } 1112 }
@@ -1152,7 +1153,6 @@ function renderPage($conf, $pluginManager)
1152 } 1153 }
1153 else // Show the configuration form. 1154 else // Show the configuration form.
1154 { 1155 {
1155 $PAGE->assign('token',getToken($conf));
1156 $PAGE->assign('title', $conf->get('general.title')); 1156 $PAGE->assign('title', $conf->get('general.title'));
1157 $PAGE->assign('redirector', $conf->get('redirector.url')); 1157 $PAGE->assign('redirector', $conf->get('redirector.url'));
1158 list($timezone_form, $timezone_js) = generateTimeZoneForm($conf->get('general.timezone')); 1158 list($timezone_form, $timezone_js) = generateTimeZoneForm($conf->get('general.timezone'));
@@ -1172,7 +1172,6 @@ function renderPage($conf, $pluginManager)
1172 if ($targetPage == Router::$PAGE_CHANGETAG) 1172 if ($targetPage == Router::$PAGE_CHANGETAG)
1173 { 1173 {
1174 if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) { 1174 if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) {
1175 $PAGE->assign('token', getToken($conf));
1176 $PAGE->assign('tags', $LINKSDB->allTags()); 1175 $PAGE->assign('tags', $LINKSDB->allTags());
1177 $PAGE->renderPage('changetag'); 1176 $PAGE->renderPage('changetag');
1178 exit; 1177 exit;
@@ -1347,7 +1346,6 @@ function renderPage($conf, $pluginManager)
1347 $data = array( 1346 $data = array(
1348 'link' => $link, 1347 'link' => $link,
1349 'link_is_new' => false, 1348 'link_is_new' => false,
1350 'token' => getToken($conf),
1351 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''), 1349 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
1352 'tags' => $LINKSDB->allTags(), 1350 'tags' => $LINKSDB->allTags(),
1353 ); 1351 );
@@ -1414,11 +1412,10 @@ function renderPage($conf, $pluginManager)
1414 $data = array( 1412 $data = array(
1415 'link' => $link, 1413 'link' => $link,
1416 'link_is_new' => $link_is_new, 1414 'link_is_new' => $link_is_new,
1417 'token' => getToken($conf), // XSRF protection.
1418 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''), 1415 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
1419 'source' => (isset($_GET['source']) ? $_GET['source'] : ''), 1416 'source' => (isset($_GET['source']) ? $_GET['source'] : ''),
1420 'tags' => $LINKSDB->allTags(), 1417 'tags' => $LINKSDB->allTags(),
1421 'default_private_links' => $conf->get('default_private_links', false), 1418 'default_private_links' => $conf->get('privacy.default_private_links', false),
1422 ); 1419 );
1423 $pluginManager->executeHooks('render_editlink', $data); 1420 $pluginManager->executeHooks('render_editlink', $data);
1424 1421
@@ -1474,27 +1471,37 @@ function renderPage($conf, $pluginManager)
1474 exit; 1471 exit;
1475 } 1472 }
1476 1473
1477 // -------- User is uploading a file for import 1474 if ($targetPage == Router::$PAGE_IMPORT) {
1478 if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=upload')) 1475 // Upload a Netscape bookmark dump to import its contents
1479 { 1476
1480 // If file is too big, some form field may be missing. 1477 if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) {
1481 if (!isset($_POST['token']) || (!isset($_FILES)) || (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size']==0)) 1478 // Show import dialog
1482 { 1479 $PAGE->assign('maxfilesize', getMaxFileSize());
1483 $returnurl = ( empty($_SERVER['HTTP_REFERER']) ? '?' : $_SERVER['HTTP_REFERER'] ); 1480 $PAGE->renderPage('import');
1484 echo '<script>alert("The file you are trying to upload is probably bigger than what this webserver can accept ('.getMaxFileSize().' bytes). Please upload in smaller chunks.");document.location=\''.escape($returnurl).'\';</script>';
1485 exit; 1481 exit;
1486 } 1482 }
1487 if (!tokenOk($_POST['token'])) die('Wrong token.');
1488 importFile($LINKSDB);
1489 exit;
1490 }
1491 1483
1492 // -------- Show upload/import dialog: 1484 // Import bookmarks from an uploaded file
1493 if ($targetPage == Router::$PAGE_IMPORT) 1485 if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) {
1494 { 1486 // The file is too big or some form field may be missing.
1495 $PAGE->assign('token',getToken($conf)); 1487 echo '<script>alert("The file you are trying to upload is probably'
1496 $PAGE->assign('maxfilesize',getMaxFileSize()); 1488 .' bigger than what this webserver can accept ('
1497 $PAGE->renderPage('import'); 1489 .getMaxFileSize().' bytes).'
1490 .' Please upload in smaller chunks.");document.location=\'?do='
1491 .Router::$PAGE_IMPORT .'\';</script>';
1492 exit;
1493 }
1494 if (! tokenOk($_POST['token'])) {
1495 die('Wrong token.');
1496 }
1497 $status = NetscapeBookmarkUtils::import(
1498 $_POST,
1499 $_FILES,
1500 $LINKSDB,
1501 $conf->get('resource.page_cache')
1502 );
1503 echo '<script>alert("'.$status.'");document.location=\'?do='
1504 .Router::$PAGE_IMPORT .'\';</script>';
1498 exit; 1505 exit;
1499 } 1506 }
1500 1507
@@ -1552,95 +1559,6 @@ function renderPage($conf, $pluginManager)
1552} 1559}
1553 1560
1554/** 1561/**
1555 * Process the import file form.
1556 *
1557 * @param LinkDB $LINKSDB Loaded LinkDB instance.
1558 * @param ConfigManager $conf Configuration Manager instance.
1559 */
1560function importFile($LINKSDB, $conf)
1561{
1562 if (!isLoggedIn()) { die('Not allowed.'); }
1563
1564 $filename=$_FILES['filetoupload']['name'];
1565 $filesize=$_FILES['filetoupload']['size'];
1566 $data=file_get_contents($_FILES['filetoupload']['tmp_name']);
1567 $private = (empty($_POST['private']) ? 0 : 1); // Should the links be imported as private?
1568 $overwrite = !empty($_POST['overwrite']) ; // Should the imported links overwrite existing ones?
1569 $import_count=0;
1570
1571 // Sniff file type:
1572 $type='unknown';
1573 if (startsWith($data,'<!DOCTYPE NETSCAPE-Bookmark-file-1>')) $type='netscape'; // Netscape bookmark file (aka Firefox).
1574
1575 // Then import the bookmarks.
1576 if ($type=='netscape')
1577 {
1578 // This is a standard Netscape-style bookmark file.
1579 // This format is supported by all browsers (except IE, of course), also Delicious, Diigo and others.
1580 foreach(explode('<DT>',$data) as $html) // explode is very fast
1581 {
1582 $link = array('linkdate'=>'','title'=>'','url'=>'','description'=>'','tags'=>'','private'=>0);
1583 $d = explode('<DD>',$html);
1584 if (startsWith($d[0], '<A '))
1585 {
1586 $link['description'] = (isset($d[1]) ? html_entity_decode(trim($d[1]),ENT_QUOTES,'UTF-8') : ''); // Get description (optional)
1587 preg_match('!<A .*?>(.*?)</A>!i',$d[0],$matches); $link['title'] = (isset($matches[1]) ? trim($matches[1]) : ''); // Get title
1588 $link['title'] = html_entity_decode($link['title'],ENT_QUOTES,'UTF-8');
1589 preg_match_all('! ([A-Z_]+)=\"(.*?)"!i',$html,$matches,PREG_SET_ORDER); // Get all other attributes
1590 $raw_add_date=0;
1591 foreach($matches as $m)
1592 {
1593 $attr=$m[1]; $value=$m[2];
1594 if ($attr=='HREF') $link['url']=html_entity_decode($value,ENT_QUOTES,'UTF-8');
1595 elseif ($attr=='ADD_DATE')
1596 {
1597 $raw_add_date=intval($value);
1598 if ($raw_add_date>30000000000) $raw_add_date/=1000; //If larger than year 2920, then was likely stored in milliseconds instead of seconds
1599 }
1600 elseif ($attr=='PRIVATE') $link['private']=($value=='0'?0:1);
1601 elseif ($attr=='TAGS') $link['tags']=html_entity_decode(str_replace(',',' ',$value),ENT_QUOTES,'UTF-8');
1602 }
1603 if ($link['url']!='')
1604 {
1605 if ($private==1) $link['private']=1;
1606 $dblink = $LINKSDB->getLinkFromUrl($link['url']); // See if the link is already in database.
1607 if ($dblink==false)
1608 { // Link not in database, let's import it...
1609 if (empty($raw_add_date)) $raw_add_date=time(); // In case of shitty bookmark file with no ADD_DATE
1610
1611 // Make sure date/time is not already used by another link.
1612 // (Some bookmark files have several different links with the same ADD_DATE)
1613 // We increment date by 1 second until we find a date which is not used in DB.
1614 // (so that links that have the same date/time are more or less kept grouped by date, but do not conflict.)
1615 while (!empty($LINKSDB[date('Ymd_His',$raw_add_date)])) { $raw_add_date++; }// Yes, I know it's ugly.
1616 $link['linkdate']=date('Ymd_His',$raw_add_date);
1617 $LINKSDB[$link['linkdate']] = $link;
1618 $import_count++;
1619 }
1620 else // Link already present in database.
1621 {
1622 if ($overwrite)
1623 { // If overwrite is required, we import link data, except date/time.
1624 $link['linkdate']=$dblink['linkdate'];
1625 $LINKSDB[$link['linkdate']] = $link;
1626 $import_count++;
1627 }
1628 }
1629
1630 }
1631 }
1632 }
1633 $LINKSDB->savedb($conf->get('resource.page_cache'));
1634
1635 echo '<script>alert("File '.json_encode($filename).' ('.$filesize.' bytes) was successfully processed: '.$import_count.' links imported.");document.location=\'?\';</script>';
1636 }
1637 else
1638 {
1639 echo '<script>alert("File '.json_encode($filename).' ('.$filesize.' bytes) has an unknown file format. Nothing was imported.");document.location=\'?\';</script>';
1640 }
1641}
1642
1643/**
1644 * Template for the list of links (<div id="linklist">) 1562 * Template for the list of links (<div id="linklist">)
1645 * This function fills all the necessary fields in the $PAGE for the template 'linklist.html' 1563 * This function fills all the necessary fields in the $PAGE for the template 'linklist.html'
1646 * 1564 *
@@ -1734,7 +1652,6 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
1734 'search_term' => $searchterm, 1652 'search_term' => $searchterm,
1735 'search_tags' => $searchtags, 1653 'search_tags' => $searchtags,
1736 'redirector' => $conf->get('redirector.url'), // Optional redirector URL. 1654 'redirector' => $conf->get('redirector.url'), // Optional redirector URL.
1737 'token' => $token,
1738 'links' => $linkDisp, 1655 'links' => $linkDisp,
1739 'tags' => $LINKSDB->allTags(), 1656 'tags' => $LINKSDB->allTags(),
1740 ); 1657 );
diff --git a/plugins/addlink_toolbar/addlink_toolbar.php b/plugins/addlink_toolbar/addlink_toolbar.php
index ba3849cf..cfd74207 100644
--- a/plugins/addlink_toolbar/addlink_toolbar.php
+++ b/plugins/addlink_toolbar/addlink_toolbar.php
@@ -35,4 +35,4 @@ function hook_addlink_toolbar_render_includes($data)
35 } 35 }
36 36
37 return $data; 37 return $data;
38} \ No newline at end of file 38}
diff --git a/plugins/playvideos/playvideos.php b/plugins/playvideos/playvideos.php
index 0a80aa58..7645b778 100644
--- a/plugins/playvideos/playvideos.php
+++ b/plugins/playvideos/playvideos.php
@@ -37,4 +37,4 @@ function hook_playvideos_render_footer($data)
37 } 37 }
38 38
39 return $data; 39 return $data;
40} \ No newline at end of file 40}
diff --git a/plugins/wallabag/README.md b/plugins/wallabag/README.md
index 3f930564..ea21a519 100644
--- a/plugins/wallabag/README.md
+++ b/plugins/wallabag/README.md
@@ -4,19 +4,19 @@ For each link in your Shaarli, adds a button to save the target page in your [wa
4 4
5### Installation 5### Installation
6 6
7Clone this repository inside your `tpl/plugins/` directory, or download the archive and unpack it there. 7Clone this repository inside your `tpl/plugins/` directory, or download the archive and unpack it there.
8The directory structure should look like: 8The directory structure should look like:
9 9
10``` 10```bash
11└── tpl 11└── tpl
12 └── plugins 12 └── plugins
13    └── wallabag 13 └── wallabag
14    ├── README.md 14 ├── README.md
15    ├── wallabag.html 15 ├── wallabag.html
16    ├── wallabag.meta 16 ├── wallabag.meta
17    ├── wallabag.php 17 ├── wallabag.php
18    ├── wallabag.php 18 ├── wallabag.php
19    └── WallabagInstance.php 19 └── WallabagInstance.php
20``` 20```
21 21
22To enable the plugin, you can either: 22To enable the plugin, you can either:
@@ -28,10 +28,10 @@ To enable the plugin, you can either:
28 28
29Go to the plugin administration page, and edit the following settings (with the plugin enabled). 29Go to the plugin administration page, and edit the following settings (with the plugin enabled).
30 30
31**WALLABAG_URL**: *Wallabag instance URL* 31**WALLABAG_URL**: *Wallabag instance URL*
32Example value: `http://v2.wallabag.org` 32Example value: `http://v2.wallabag.org`
33 33
34**WALLABAG_VERSION**: *Wallabag version* 34**WALLABAG_VERSION**: *Wallabag version*
35Value: either `1` (for 1.x) or `2` (for 2.x) 35Value: either `1` (for 1.x) or `2` (for 2.x)
36 36
37> Note: these settings can also be set in `data/config.json.php`, in the plugins section. \ No newline at end of file 37> Note: these settings can also be set in `data/config.json.php`, in the plugins section.
diff --git a/tests/LanguagesTest.php b/tests/LanguagesTest.php
new file mode 100644
index 00000000..79c136c8
--- /dev/null
+++ b/tests/LanguagesTest.php
@@ -0,0 +1,41 @@
1<?php
2
3require_once 'application/Languages.php';
4
5/**
6 * Class LanguagesTest.
7 */
8class LanguagesTest extends PHPUnit_Framework_TestCase
9{
10 /**
11 * Test t() with a simple non identified value.
12 */
13 public function testTranslateSingleNotID()
14 {
15 $text = 'abcdé 564 fgK';
16 $this->assertEquals($text, t($text));
17 }
18
19 /**
20 * Test t() with a non identified plural form.
21 */
22 public function testTranslatePluralNotID()
23 {
24 $text = '%s sandwich';
25 $nText = '%s sandwiches';
26 $this->assertEquals('0 sandwich', t($text, $nText));
27 $this->assertEquals('1 sandwich', t($text, $nText, 1));
28 $this->assertEquals('2 sandwiches', t($text, $nText, 2));
29 }
30
31 /**
32 * Test t() with a non identified invalid plural form.
33 */
34 public function testTranslatePluralNotIDInvalid()
35 {
36 $text = 'sandwich';
37 $nText = 'sandwiches';
38 $this->assertEquals('sandwich', t($text, $nText, 1));
39 $this->assertEquals('sandwiches', t($text, $nText, 2));
40 }
41}
diff --git a/tests/LinkDBTest.php b/tests/LinkDBTest.php
index 46956f20..31306069 100644
--- a/tests/LinkDBTest.php
+++ b/tests/LinkDBTest.php
@@ -317,6 +317,10 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
317 '-exclude' => 1, 317 '-exclude' => 1,
318 '.hidden' => 1, 318 '.hidden' => 1,
319 'hashtag' => 2, 319 'hashtag' => 2,
320 'tag1' => 1,
321 'tag2' => 1,
322 'tag3' => 1,
323 'tag4' => 1,
320 ), 324 ),
321 self::$privateLinkDB->allTags() 325 self::$privateLinkDB->allTags()
322 ); 326 );
diff --git a/tests/NetscapeBookmarkUtilsTest.php b/tests/NetscapeBookmarkUtils/BookmarkExportTest.php
index 41e6d84c..cc54ab9f 100644
--- a/tests/NetscapeBookmarkUtilsTest.php
+++ b/tests/NetscapeBookmarkUtils/BookmarkExportTest.php
@@ -3,9 +3,9 @@
3require_once 'application/NetscapeBookmarkUtils.php'; 3require_once 'application/NetscapeBookmarkUtils.php';
4 4
5/** 5/**
6 * Netscape bookmark import and export 6 * Netscape bookmark export
7 */ 7 */
8class NetscapeBookmarkUtilsTest extends PHPUnit_Framework_TestCase 8class BookmarkExportTest extends PHPUnit_Framework_TestCase
9{ 9{
10 /** 10 /**
11 * @var string datastore to test write operations 11 * @var string datastore to test write operations
diff --git a/tests/NetscapeBookmarkUtils/BookmarkImportTest.php b/tests/NetscapeBookmarkUtils/BookmarkImportTest.php
new file mode 100644
index 00000000..f0ad500f
--- /dev/null
+++ b/tests/NetscapeBookmarkUtils/BookmarkImportTest.php
@@ -0,0 +1,546 @@
1<?php
2
3require_once 'application/NetscapeBookmarkUtils.php';
4
5
6/**
7 * Utility function to load a file's metadata in a $_FILES-like array
8 *
9 * @param string $filename Basename of the file
10 *
11 * @return array A $_FILES-like array
12 */
13function file2array($filename)
14{
15 return array(
16 'filetoupload' => array(
17 'name' => $filename,
18 'tmp_name' => __DIR__ . '/input/' . $filename,
19 'size' => filesize(__DIR__ . '/input/' . $filename)
20 )
21 );
22}
23
24
25/**
26 * Netscape bookmark import
27 */
28class BookmarkImportTest extends PHPUnit_Framework_TestCase
29{
30 /**
31 * @var string datastore to test write operations
32 */
33 protected static $testDatastore = 'sandbox/datastore.php';
34
35 /**
36 * @var LinkDB private LinkDB instance
37 */
38 protected $linkDb = null;
39
40 /**
41 * @var string Dummy page cache
42 */
43 protected $pagecache = 'tests';
44
45 /**
46 * Resets test data before each test
47 */
48 protected function setUp()
49 {
50 if (file_exists(self::$testDatastore)) {
51 unlink(self::$testDatastore);
52 }
53 // start with an empty datastore
54 file_put_contents(self::$testDatastore, '<?php /* S7QysKquBQA= */ ?>');
55 $this->linkDb = new LinkDB(self::$testDatastore, true, false);
56 }
57
58 /**
59 * Attempt to import bookmarks from an empty file
60 */
61 public function testImportEmptyData()
62 {
63 $files = file2array('empty.htm');
64 $this->assertEquals(
65 'File empty.htm (0 bytes) has an unknown file format.'
66 .' Nothing was imported.',
67 NetscapeBookmarkUtils::import(NULL, $files, NULL, NULL)
68 );
69 $this->assertEquals(0, count($this->linkDb));
70 }
71
72 /**
73 * Attempt to import bookmarks from a file with no Doctype
74 */
75 public function testImportNoDoctype()
76 {
77 $files = file2array('no_doctype.htm');
78 $this->assertEquals(
79 'File no_doctype.htm (350 bytes) has an unknown file format. Nothing was imported.',
80 NetscapeBookmarkUtils::import(NULL, $files, NULL, NULL)
81 );
82 $this->assertEquals(0, count($this->linkDb));
83 }
84
85 /**
86 * Ensure IE dumps are supported
87 */
88 public function testImportInternetExplorerEncoding()
89 {
90 $files = file2array('internet_explorer_encoding.htm');
91 $this->assertEquals(
92 'File internet_explorer_encoding.htm (356 bytes) was successfully processed:'
93 .' 1 links imported, 0 links overwritten, 0 links skipped.',
94 NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->pagecache)
95 );
96 $this->assertEquals(1, count($this->linkDb));
97 $this->assertEquals(0, count_private($this->linkDb));
98
99 $this->assertEquals(
100 array(
101 'linkdate' => '20160618_173944',
102 'title' => 'Hg Init a Mercurial tutorial by Joel Spolsky',
103 'url' => 'http://hginit.com/',
104 'description' => '',
105 'private' => 0,
106 'tags' => ''
107 ),
108 $this->linkDb->getLinkFromUrl('http://hginit.com/')
109 );
110 }
111
112
113 /**
114 * Import bookmarks nested in a folder hierarchy
115 */
116 public function testImportNested()
117 {
118 $files = file2array('netscape_nested.htm');
119 $this->assertEquals(
120 'File netscape_nested.htm (1337 bytes) was successfully processed:'
121 .' 8 links imported, 0 links overwritten, 0 links skipped.',
122 NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->pagecache)
123 );
124 $this->assertEquals(8, count($this->linkDb));
125 $this->assertEquals(2, count_private($this->linkDb));
126
127 $this->assertEquals(
128 array(
129 'linkdate' => '20160225_205541',
130 'title' => 'Nested 1',
131 'url' => 'http://nest.ed/1',
132 'description' => '',
133 'private' => 0,
134 'tags' => 'tag1 tag2'
135 ),
136 $this->linkDb->getLinkFromUrl('http://nest.ed/1')
137 );
138 $this->assertEquals(
139 array(
140 'linkdate' => '20160225_205542',
141 'title' => 'Nested 1-1',
142 'url' => 'http://nest.ed/1-1',
143 'description' => '',
144 'private' => 0,
145 'tags' => 'folder1 tag1 tag2'
146 ),
147 $this->linkDb->getLinkFromUrl('http://nest.ed/1-1')
148 );
149 $this->assertEquals(
150 array(
151 'linkdate' => '20160225_205547',
152 'title' => 'Nested 1-2',
153 'url' => 'http://nest.ed/1-2',
154 'description' => '',
155 'private' => 0,
156 'tags' => 'folder1 tag3 tag4'
157 ),
158 $this->linkDb->getLinkFromUrl('http://nest.ed/1-2')
159 );
160 $this->assertEquals(
161 array(
162 'linkdate' => '20160202_172222',
163 'title' => 'Nested 2-1',
164 'url' => 'http://nest.ed/2-1',
165 'description' => 'First link of the second section',
166 'private' => 1,
167 'tags' => 'folder2'
168 ),
169 $this->linkDb->getLinkFromUrl('http://nest.ed/2-1')
170 );
171 $this->assertEquals(
172 array(
173 'linkdate' => '20160119_200227',
174 'title' => 'Nested 2-2',
175 'url' => 'http://nest.ed/2-2',
176 'description' => 'Second link of the second section',
177 'private' => 1,
178 'tags' => 'folder2'
179 ),
180 $this->linkDb->getLinkFromUrl('http://nest.ed/2-2')
181 );
182 $this->assertEquals(
183 array(
184 'linkdate' => '20160202_172223',
185 'title' => 'Nested 3-1',
186 'url' => 'http://nest.ed/3-1',
187 'description' => '',
188 'private' => 0,
189 'tags' => 'folder3 folder3-1 tag3'
190 ),
191 $this->linkDb->getLinkFromUrl('http://nest.ed/3-1')
192 );
193 $this->assertEquals(
194 array(
195 'linkdate' => '20160119_200228',
196 'title' => 'Nested 3-2',
197 'url' => 'http://nest.ed/3-2',
198 'description' => '',
199 'private' => 0,
200 'tags' => 'folder3 folder3-1'
201 ),
202 $this->linkDb->getLinkFromUrl('http://nest.ed/3-2')
203 );
204 $this->assertEquals(
205 array(
206 'linkdate' => '20160229_081541',
207 'title' => 'Nested 2',
208 'url' => 'http://nest.ed/2',
209 'description' => '',
210 'private' => 0,
211 'tags' => 'tag4'
212 ),
213 $this->linkDb->getLinkFromUrl('http://nest.ed/2')
214 );
215 }
216
217 /**
218 * Import bookmarks with the default privacy setting (reuse from file)
219 *
220 * The $_POST array is not set.
221 */
222 public function testImportDefaultPrivacyNoPost()
223 {
224 $files = file2array('netscape_basic.htm');
225 $this->assertEquals(
226 'File netscape_basic.htm (482 bytes) was successfully processed:'
227 .' 2 links imported, 0 links overwritten, 0 links skipped.',
228 NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->pagecache)
229 );
230 $this->assertEquals(2, count($this->linkDb));
231 $this->assertEquals(1, count_private($this->linkDb));
232
233 $this->assertEquals(
234 array(
235 'linkdate' => '20001010_105536',
236 'title' => 'Secret stuff',
237 'url' => 'https://private.tld',
238 'description' => "Super-secret stuff you're not supposed to know about",
239 'private' => 1,
240 'tags' => 'private secret'
241 ),
242 $this->linkDb->getLinkFromUrl('https://private.tld')
243 );
244 $this->assertEquals(
245 array(
246 'linkdate' => '20160225_205548',
247 'title' => 'Public stuff',
248 'url' => 'http://public.tld',
249 'description' => '',
250 'private' => 0,
251 'tags' => 'public hello world'
252 ),
253 $this->linkDb->getLinkFromUrl('http://public.tld')
254 );
255 }
256
257 /**
258 * Import bookmarks with the default privacy setting (reuse from file)
259 */
260 public function testImportKeepPrivacy()
261 {
262 $post = array('privacy' => 'default');
263 $files = file2array('netscape_basic.htm');
264 $this->assertEquals(
265 'File netscape_basic.htm (482 bytes) was successfully processed:'
266 .' 2 links imported, 0 links overwritten, 0 links skipped.',
267 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
268 );
269 $this->assertEquals(2, count($this->linkDb));
270 $this->assertEquals(1, count_private($this->linkDb));
271
272 $this->assertEquals(
273 array(
274 'linkdate' => '20001010_105536',
275 'title' => 'Secret stuff',
276 'url' => 'https://private.tld',
277 'description' => "Super-secret stuff you're not supposed to know about",
278 'private' => 1,
279 'tags' => 'private secret'
280 ),
281 $this->linkDb->getLinkFromUrl('https://private.tld')
282 );
283 $this->assertEquals(
284 array(
285 'linkdate' => '20160225_205548',
286 'title' => 'Public stuff',
287 'url' => 'http://public.tld',
288 'description' => '',
289 'private' => 0,
290 'tags' => 'public hello world'
291 ),
292 $this->linkDb->getLinkFromUrl('http://public.tld')
293 );
294 }
295
296 /**
297 * Import links as public
298 */
299 public function testImportAsPublic()
300 {
301 $post = array('privacy' => 'public');
302 $files = file2array('netscape_basic.htm');
303 $this->assertEquals(
304 'File netscape_basic.htm (482 bytes) was successfully processed:'
305 .' 2 links imported, 0 links overwritten, 0 links skipped.',
306 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
307 );
308 $this->assertEquals(2, count($this->linkDb));
309 $this->assertEquals(0, count_private($this->linkDb));
310 $this->assertEquals(
311 0,
312 $this->linkDb['20001010_105536']['private']
313 );
314 $this->assertEquals(
315 0,
316 $this->linkDb['20160225_205548']['private']
317 );
318 }
319
320 /**
321 * Import links as private
322 */
323 public function testImportAsPrivate()
324 {
325 $post = array('privacy' => 'private');
326 $files = file2array('netscape_basic.htm');
327 $this->assertEquals(
328 'File netscape_basic.htm (482 bytes) was successfully processed:'
329 .' 2 links imported, 0 links overwritten, 0 links skipped.',
330 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
331 );
332 $this->assertEquals(2, count($this->linkDb));
333 $this->assertEquals(2, count_private($this->linkDb));
334 $this->assertEquals(
335 1,
336 $this->linkDb['20001010_105536']['private']
337 );
338 $this->assertEquals(
339 1,
340 $this->linkDb['20160225_205548']['private']
341 );
342 }
343
344 /**
345 * Overwrite private links so they become public
346 */
347 public function testOverwriteAsPublic()
348 {
349 $files = file2array('netscape_basic.htm');
350
351 // import links as private
352 $post = array('privacy' => 'private');
353 $this->assertEquals(
354 'File netscape_basic.htm (482 bytes) was successfully processed:'
355 .' 2 links imported, 0 links overwritten, 0 links skipped.',
356 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
357 );
358 $this->assertEquals(2, count($this->linkDb));
359 $this->assertEquals(2, count_private($this->linkDb));
360 $this->assertEquals(
361 1,
362 $this->linkDb['20001010_105536']['private']
363 );
364 $this->assertEquals(
365 1,
366 $this->linkDb['20160225_205548']['private']
367 );
368
369 // re-import as public, enable overwriting
370 $post = array(
371 'privacy' => 'public',
372 'overwrite' => 'true'
373 );
374 $this->assertEquals(
375 'File netscape_basic.htm (482 bytes) was successfully processed:'
376 .' 2 links imported, 2 links overwritten, 0 links skipped.',
377 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
378 );
379 $this->assertEquals(2, count($this->linkDb));
380 $this->assertEquals(0, count_private($this->linkDb));
381 $this->assertEquals(
382 0,
383 $this->linkDb['20001010_105536']['private']
384 );
385 $this->assertEquals(
386 0,
387 $this->linkDb['20160225_205548']['private']
388 );
389 }
390
391 /**
392 * Overwrite public links so they become private
393 */
394 public function testOverwriteAsPrivate()
395 {
396 $files = file2array('netscape_basic.htm');
397
398 // import links as public
399 $post = array('privacy' => 'public');
400 $this->assertEquals(
401 'File netscape_basic.htm (482 bytes) was successfully processed:'
402 .' 2 links imported, 0 links overwritten, 0 links skipped.',
403 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
404 );
405 $this->assertEquals(2, count($this->linkDb));
406 $this->assertEquals(0, count_private($this->linkDb));
407 $this->assertEquals(
408 0,
409 $this->linkDb['20001010_105536']['private']
410 );
411 $this->assertEquals(
412 0,
413 $this->linkDb['20160225_205548']['private']
414 );
415
416 // re-import as private, enable overwriting
417 $post = array(
418 'privacy' => 'private',
419 'overwrite' => 'true'
420 );
421 $this->assertEquals(
422 'File netscape_basic.htm (482 bytes) was successfully processed:'
423 .' 2 links imported, 2 links overwritten, 0 links skipped.',
424 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
425 );
426 $this->assertEquals(2, count($this->linkDb));
427 $this->assertEquals(2, count_private($this->linkDb));
428 $this->assertEquals(
429 1,
430 $this->linkDb['20001010_105536']['private']
431 );
432 $this->assertEquals(
433 1,
434 $this->linkDb['20160225_205548']['private']
435 );
436 }
437
438 /**
439 * Attept to import the same links twice without enabling overwriting
440 */
441 public function testSkipOverwrite()
442 {
443 $post = array('privacy' => 'public');
444 $files = file2array('netscape_basic.htm');
445 $this->assertEquals(
446 'File netscape_basic.htm (482 bytes) was successfully processed:'
447 .' 2 links imported, 0 links overwritten, 0 links skipped.',
448 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
449 );
450 $this->assertEquals(2, count($this->linkDb));
451 $this->assertEquals(0, count_private($this->linkDb));
452
453 // re-import as private, DO NOT enable overwriting
454 $post = array('privacy' => 'private');
455 $this->assertEquals(
456 'File netscape_basic.htm (482 bytes) was successfully processed:'
457 .' 0 links imported, 0 links overwritten, 2 links skipped.',
458 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
459 );
460 $this->assertEquals(2, count($this->linkDb));
461 $this->assertEquals(0, count_private($this->linkDb));
462 }
463
464 /**
465 * Add user-specified tags to all imported bookmarks
466 */
467 public function testSetDefaultTags()
468 {
469 $post = array(
470 'privacy' => 'public',
471 'default_tags' => 'tag1,tag2 tag3'
472 );
473 $files = file2array('netscape_basic.htm');
474 $this->assertEquals(
475 'File netscape_basic.htm (482 bytes) was successfully processed:'
476 .' 2 links imported, 0 links overwritten, 0 links skipped.',
477 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
478 );
479 $this->assertEquals(2, count($this->linkDb));
480 $this->assertEquals(0, count_private($this->linkDb));
481 $this->assertEquals(
482 'tag1 tag2 tag3 private secret',
483 $this->linkDb['20001010_105536']['tags']
484 );
485 $this->assertEquals(
486 'tag1 tag2 tag3 public hello world',
487 $this->linkDb['20160225_205548']['tags']
488 );
489 }
490
491 /**
492 * The user-specified tags contain characters to be escaped
493 */
494 public function testSanitizeDefaultTags()
495 {
496 $post = array(
497 'privacy' => 'public',
498 'default_tags' => 'tag1&,tag2 "tag3"'
499 );
500 $files = file2array('netscape_basic.htm');
501 $this->assertEquals(
502 'File netscape_basic.htm (482 bytes) was successfully processed:'
503 .' 2 links imported, 0 links overwritten, 0 links skipped.',
504 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->pagecache)
505 );
506 $this->assertEquals(2, count($this->linkDb));
507 $this->assertEquals(0, count_private($this->linkDb));
508 $this->assertEquals(
509 'tag1&amp; tag2 &quot;tag3&quot; private secret',
510 $this->linkDb['20001010_105536']['tags']
511 );
512 $this->assertEquals(
513 'tag1&amp; tag2 &quot;tag3&quot; public hello world',
514 $this->linkDb['20160225_205548']['tags']
515 );
516 }
517
518 /**
519 * Ensure each imported bookmark has a unique linkdate
520 *
521 * See https://github.com/shaarli/Shaarli/issues/351
522 */
523 public function testImportSameDate()
524 {
525 $files = file2array('same_date.htm');
526 $this->assertEquals(
527 'File same_date.htm (453 bytes) was successfully processed:'
528 .' 3 links imported, 0 links overwritten, 0 links skipped.',
529 NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->pagecache)
530 );
531 $this->assertEquals(3, count($this->linkDb));
532 $this->assertEquals(0, count_private($this->linkDb));
533 $this->assertEquals(
534 '20160225_205548',
535 $this->linkDb['20160225_205548']['linkdate']
536 );
537 $this->assertEquals(
538 '20160225_205549',
539 $this->linkDb['20160225_205549']['linkdate']
540 );
541 $this->assertEquals(
542 '20160225_205550',
543 $this->linkDb['20160225_205550']['linkdate']
544 );
545 }
546}
diff --git a/tests/NetscapeBookmarkUtils/input/empty.htm b/tests/NetscapeBookmarkUtils/input/empty.htm
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/NetscapeBookmarkUtils/input/empty.htm
diff --git a/tests/NetscapeBookmarkUtils/input/internet_explorer_encoding.htm b/tests/NetscapeBookmarkUtils/input/internet_explorer_encoding.htm
new file mode 100644
index 00000000..18703cf6
--- /dev/null
+++ b/tests/NetscapeBookmarkUtils/input/internet_explorer_encoding.htm
@@ -0,0 +1,9 @@
1<!DOCTYPE NETSCAPE-Bookmark-file-1>
2<!-- This is an automatically generated file.
3It will be read and overwritten.
4Do Not Edit! -->
5<TITLE>Bookmarks</TITLE>
6<H1>Bookmarks</H1>
7<DL><p>
8 <DT><A HREF="http://hginit.com/" ADD_DATE="1466271584" LAST_VISIT="1466271584" LAST_MODIFIED="1466271584" >Hg Init a Mercurial tutorial by Joel Spolsky</A>
9</DL><p>
diff --git a/tests/NetscapeBookmarkUtils/input/netscape_basic.htm b/tests/NetscapeBookmarkUtils/input/netscape_basic.htm
new file mode 100644
index 00000000..affe0cf8
--- /dev/null
+++ b/tests/NetscapeBookmarkUtils/input/netscape_basic.htm
@@ -0,0 +1,11 @@
1<!DOCTYPE NETSCAPE-Bookmark-file-1>
2<!-- This is an automatically generated file.
3It will be read and overwritten.
4Do Not Edit! -->
5<TITLE>Bookmarks</TITLE>
6<H1>Bookmarks</H1>
7<DL><p>
8<DT><A HREF="https://private.tld" ADD_DATE="10/Oct/2000:13:55:36 +0300" PRIVATE="1" TAGS="private secret">Secret stuff</A>
9<DD>Super-secret stuff you're not supposed to know about
10<DT><A HREF="http://public.tld" ADD_DATE="1456433748" PRIVATE="0" TAGS="public hello world">Public stuff</A>
11</DL><p>
diff --git a/tests/NetscapeBookmarkUtils/input/netscape_nested.htm b/tests/NetscapeBookmarkUtils/input/netscape_nested.htm
new file mode 100644
index 00000000..b486fe18
--- /dev/null
+++ b/tests/NetscapeBookmarkUtils/input/netscape_nested.htm
@@ -0,0 +1,31 @@
1<!DOCTYPE NETSCAPE-Bookmark-file-1>
2<!-- This is an automatically generated file.
3It will be read and overwritten.
4Do Not Edit! -->
5<TITLE>Bookmarks</TITLE>
6<H1>Bookmarks</H1>
7<DL><p>
8 <DT><A HREF="http://nest.ed/1" ADD_DATE="1456433741" PRIVATE="0" TAGS="tag1,tag2">Nested 1</A>
9 <DT><H3 ADD_DATE="1456433722" LAST_MODIFIED="1456433739">Folder1</H3>
10 <DL><p>
11 <DT><A HREF="http://nest.ed/1-1" ADD_DATE="1456433742" PRIVATE="0" TAGS="tag1,tag2">Nested 1-1</A>
12 <DT><A HREF="http://nest.ed/1-2" ADD_DATE="1456433747" PRIVATE="0" TAGS="tag3,tag4">Nested 1-2</A>
13 </DL><p>
14 <DT><H3 ADD_DATE="1456433722">Folder2</H3>
15 <DD>This second folder contains wonderful links!
16 <DL><p>
17 <DT><A HREF="http://nest.ed/2-1" ADD_DATE="1454433742" PRIVATE="1">Nested 2-1</A>
18 <DD>First link of the second section
19 <DT><A HREF="http://nest.ed/2-2" ADD_DATE="1453233747" PRIVATE="1">Nested 2-2</A>
20 <DD>Second link of the second section
21 </DL><p>
22 <DT><H3>Folder3</H3>
23 <DL><p>
24 <DT><H3>Folder3-1</H3>
25 <DL><p>
26 <DT><A HREF="http://nest.ed/3-1" ADD_DATE="1454433742" PRIVATE="0" TAGS="tag3">Nested 3-1</A>
27 <DT><A HREF="http://nest.ed/3-2" ADD_DATE="1453233747" PRIVATE="0">Nested 3-2</A>
28 </DL><p>
29 </DL><p>
30 <DT><A HREF="http://nest.ed/2" ADD_DATE="1456733741" PRIVATE="0" TAGS="tag4">Nested 2</A>
31</DL><p>
diff --git a/tests/NetscapeBookmarkUtils/input/no_doctype.htm b/tests/NetscapeBookmarkUtils/input/no_doctype.htm
new file mode 100644
index 00000000..766d398b
--- /dev/null
+++ b/tests/NetscapeBookmarkUtils/input/no_doctype.htm
@@ -0,0 +1,7 @@
1<TITLE>Bookmarks</TITLE>
2<H1>Bookmarks</H1>
3<DL><p>
4<DT><A HREF="https://private.tld" ADD_DATE="10/Oct/2000:13:55:36 +0300" PRIVATE="1" TAGS="private secret">Secret stuff</A>
5<DD>Super-secret stuff you're not supposed to know about
6<DT><A HREF="http://public.tld" ADD_DATE="1456433748" PRIVATE="0" TAGS="public hello world">Public stuff</A>
7</DL><p>
diff --git a/tests/NetscapeBookmarkUtils/input/same_date.htm b/tests/NetscapeBookmarkUtils/input/same_date.htm
new file mode 100644
index 00000000..9d58a582
--- /dev/null
+++ b/tests/NetscapeBookmarkUtils/input/same_date.htm
@@ -0,0 +1,11 @@
1<!DOCTYPE NETSCAPE-Bookmark-file-1>
2<!-- This is an automatically generated file.
3It will be read and overwritten.
4Do Not Edit! -->
5<TITLE>Bookmarks</TITLE>
6<H1>Bookmarks</H1>
7<DL><p>
8<DT><A HREF="https://fir.st" ADD_DATE="1456433748" PRIVATE="0">Today's first link</A>
9<DT><A HREF="https://seco.nd" ADD_DATE="1456433748" PRIVATE="0">Today's second link</A>
10<DT><A HREF="https://thi.rd" ADD_DATE="1456433748" PRIVATE="0">Today's third link</A>
11</DL><p>
diff --git a/tests/PluginManagerTest.php b/tests/PluginManagerTest.php
index f4826e2e..ddf48185 100644
--- a/tests/PluginManagerTest.php
+++ b/tests/PluginManagerTest.php
@@ -92,4 +92,4 @@ class PluginManagerTest extends PHPUnit_Framework_TestCase
92 $this->assertEquals('test plugin', $meta[self::$pluginName]['description']); 92 $this->assertEquals('test plugin', $meta[self::$pluginName]['description']);
93 $this->assertEquals($expectedParameters, $meta[self::$pluginName]['parameters']); 93 $this->assertEquals($expectedParameters, $meta[self::$pluginName]['parameters']);
94 } 94 }
95} \ No newline at end of file 95}
diff --git a/tests/Updater/UpdaterTest.php b/tests/Updater/UpdaterTest.php
index 6bdce08b..0d0ad922 100644
--- a/tests/Updater/UpdaterTest.php
+++ b/tests/Updater/UpdaterTest.php
@@ -263,4 +263,28 @@ $GLOBALS[\'privateLinkByDefault\'] = true;';
263 $expected = filemtime($this->conf->getConfigFileExt()); 263 $expected = filemtime($this->conf->getConfigFileExt());
264 $this->assertEquals($expected, $filetime); 264 $this->assertEquals($expected, $filetime);
265 } 265 }
266
267 /**
268 * Test escapeUnescapedConfig with valid data.
269 */
270 public function testEscapeConfig()
271 {
272 $sandbox = 'sandbox/config';
273 copy(self::$configFile .'.json.php', $sandbox .'.json.php');
274 $this->conf = new ConfigManager($sandbox);
275 $title = '<script>alert("title");</script>';
276 $headerLink = '<script>alert("header_link");</script>';
277 $redirectorUrl = '<script>alert("redirector");</script>';
278 $this->conf->set('general.title', $title);
279 $this->conf->set('general.header_link', $headerLink);
280 $this->conf->set('redirector.url', $redirectorUrl);
281 $updater = new Updater(array(), array(), $this->conf, true);
282 $done = $updater->updateMethodEscapeUnescapedConfig();
283 $this->assertTrue($done);
284 $this->conf->reload();
285 $this->assertEquals(escape($title), $this->conf->get('general.title'));
286 $this->assertEquals(escape($headerLink), $this->conf->get('general.header_link'));
287 $this->assertEquals(escape($redirectorUrl), $this->conf->get('redirector.url'));
288 unlink($sandbox .'.json.php');
289 }
266} 290}
diff --git a/tests/utils/ReferenceLinkDB.php b/tests/utils/ReferenceLinkDB.php
index fe16baac..fcc7a4f9 100644
--- a/tests/utils/ReferenceLinkDB.php
+++ b/tests/utils/ReferenceLinkDB.php
@@ -75,7 +75,7 @@ class ReferenceLinkDB
75 '', 75 '',
76 1, 76 1,
77 '20121206_182539', 77 '20121206_182539',
78 'dev cartoon' 78 'dev cartoon tag1 tag2 tag3 tag4 '
79 ); 79 );
80 } 80 }
81 81
diff --git a/tests/utils/config/configInvalid.json.php b/tests/utils/config/configInvalid.json.php
index 167f2168..e39d640a 100644
--- a/tests/utils/config/configInvalid.json.php
+++ b/tests/utils/config/configInvalid.json.php
@@ -2,4 +2,4 @@
2{ 2{
3 bad: bad, 3 bad: bad,
4} 4}
5*/ ?> \ No newline at end of file 5*/ ?>
diff --git a/tpl/import.html b/tpl/import.html
index 6c4f9421..071e1160 100644
--- a/tpl/import.html
+++ b/tpl/import.html
@@ -3,19 +3,31 @@
3<head>{include="includes"}</head> 3<head>{include="includes"}</head>
4<body onload="document.uploadform.filetoupload.focus();"> 4<body onload="document.uploadform.filetoupload.focus();">
5<div id="pageheader"> 5<div id="pageheader">
6 {include="page.header"} 6 {include="page.header"}
7 <div id="uploaddiv"> 7 <div id="uploaddiv">
8 Import Netscape HTML bookmarks (as exported from Firefox/Chrome/Opera/Delicious/Diigo...) (Max: {$maxfilesize} bytes). 8 Import Netscape HTML bookmarks (as exported from Firefox/Chrome/Opera/Delicious/Diigo...) (Max: {$maxfilesize} bytes).
9 <form method="POST" action="?do=upload" enctype="multipart/form-data" name="uploadform" id="uploadform"> 9 <form method="POST" action="?do=import" enctype="multipart/form-data"
10 <input type="hidden" name="token" value="{$token}"> 10 name="uploadform" id="uploadform">
11 <input type="file" name="filetoupload"> 11 <input type="hidden" name="token" value="{$token}">
12 <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}"> 12 <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}">
13 <input type="submit" name="import_file" value="Import" class="bigbutton"><br> 13 <input type="file" name="filetoupload">
14 <input type="checkbox" name="private" id="private"><label for="private">&nbsp;Import all links as private</label><br> 14 <input type="submit" name="import_file" value="Import" class="bigbutton"><br>
15 <input type="checkbox" name="overwrite" id="overwrite"><label for="overwrite">&nbsp;Overwrite existing links</label> 15
16 </form> 16 <label for="privacy">&nbsp;Visibility:</label><br>
17 </div> 17 <input type="radio" name="privacy" value="default" checked="true">
18 &nbsp;Use values from the imported file, default to public<br>
19 <input type="radio" name="privacy" value="private">
20 &nbsp;Import all bookmarks as private<br>
21 <input type="radio" name="privacy" value="public">
22 &nbsp;Import all bookmarks as public<br>
23
24 <input type="checkbox" name="overwrite" id="overwrite">
25 <label for="overwrite">&nbsp;Overwrite existing bookmarks</label><br>
26 <label for="default_tags">&nbsp;Add default tags</label>
27 <input type="text" name="default_tags" id="default_tags">
28 </form>
29 </div>
18</div> 30</div>
19{include="page.footer"} 31{include="page.footer"}
20</body> 32</body>
21</html> \ No newline at end of file 33</html>
diff --git a/tpl/tools.html b/tpl/tools.html
index 9e45caad..8e285f44 100644
--- a/tpl/tools.html
+++ b/tpl/tools.html
@@ -9,7 +9,7 @@
9 <br><br> 9 <br><br>
10 <a href="?do=pluginadmin"><b>Plugin administration</b><span>: Enable, disable and configure plugins.</span></a> 10 <a href="?do=pluginadmin"><b>Plugin administration</b><span>: Enable, disable and configure plugins.</span></a>
11 <br><br> 11 <br><br>
12 {if="$openshaarli"}<a href="?do=changepasswd"><b>Change password</b><span>: Change your password.</span></a> 12 {if="!$openshaarli"}<a href="?do=changepasswd"><b>Change password</b><span>: Change your password.</span></a>
13 <br><br>{/if} 13 <br><br>{/if}
14 <a href="?do=changetag"><b>Rename/delete tags</b><span>: Rename or delete a tag in all links</span></a> 14 <a href="?do=changetag"><b>Rename/delete tags</b><span>: Rename or delete a tag in all links</span></a>
15 <br><br> 15 <br><br>