aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--application/FileUtils.php75
-rw-r--r--application/History.php200
-rw-r--r--application/LinkDB.php35
-rw-r--r--application/NetscapeBookmarkUtils.php5
-rw-r--r--application/PageBuilder.php2
-rw-r--r--application/TimeZone.php101
-rw-r--r--application/Utils.php90
-rw-r--r--application/config/ConfigManager.php1
-rw-r--r--application/exceptions/IOException.php22
-rw-r--r--index.php91
-rw-r--r--plugins/readityourself/book-open.pngbin568 -> 0 bytes
-rw-r--r--plugins/readityourself/readityourself.html1
-rw-r--r--plugins/readityourself/readityourself.meta2
-rw-r--r--plugins/readityourself/readityourself.php51
-rw-r--r--tests/FileUtilsTest.php108
-rw-r--r--tests/HistoryTest.php207
-rw-r--r--tests/LinkDBTest.php2
-rw-r--r--tests/NetscapeBookmarkUtils/BookmarkImportTest.php81
-rw-r--r--tests/TimeZoneTest.php83
-rw-r--r--tests/UtilsTest.php91
-rw-r--r--tests/plugins/PluginReadityourselfTest.php99
-rw-r--r--tpl/default/configure.html30
-rw-r--r--tpl/default/import.html1
-rw-r--r--tpl/default/install.html42
-rw-r--r--tpl/default/js/shaarli.js55
-rw-r--r--tpl/vintage/configure.html26
-rw-r--r--tpl/vintage/css/shaarli.css4
-rw-r--r--tpl/vintage/import.html2
-rw-r--r--tpl/vintage/install.html28
-rw-r--r--tpl/vintage/js/shaarli.js32
-rw-r--r--tpl/vintage/page.footer.html1
-rw-r--r--tpl/vintage/page.header.html4
32 files changed, 1185 insertions, 387 deletions
diff --git a/application/FileUtils.php b/application/FileUtils.php
index 6cac9825..b8ad8970 100644
--- a/application/FileUtils.php
+++ b/application/FileUtils.php
@@ -1,21 +1,76 @@
1<?php 1<?php
2
3require_once 'exceptions/IOException.php';
4
2/** 5/**
3 * Exception class thrown when a filesystem access failure happens 6 * Class FileUtils
7 *
8 * Utility class for file manipulation.
4 */ 9 */
5class IOException extends Exception 10class FileUtils
6{ 11{
7 private $path; 12 /**
13 * @var string
14 */
15 protected static $phpPrefix = '<?php /* ';
16
17 /**
18 * @var string
19 */
20 protected static $phpSuffix = ' */ ?>';
8 21
9 /** 22 /**
10 * Construct a new IOException 23 * Write data into a file (Shaarli database format).
24 * The data is stored in a PHP file, as a comment, in compressed base64 format.
25 *
26 * The file will be created if it doesn't exist.
27 *
28 * @param string $file File path.
29 * @param string $content Content to write.
30 *
31 * @return int|bool Number of bytes written or false if it fails.
11 * 32 *
12 * @param string $path path to the resource that cannot be accessed 33 * @throws IOException The destination file can't be written.
13 * @param string $message Custom exception message.
14 */ 34 */
15 public function __construct($path, $message = '') 35 public static function writeFlatDB($file, $content)
16 { 36 {
17 $this->path = $path; 37 if (is_file($file) && !is_writeable($file)) {
18 $this->message = empty($message) ? 'Error accessing' : $message; 38 // The datastore exists but is not writeable
19 $this->message .= PHP_EOL . $this->path; 39 throw new IOException($file);
40 } else if (!is_file($file) && !is_writeable(dirname($file))) {
41 // The datastore does not exist and its parent directory is not writeable
42 throw new IOException(dirname($file));
43 }
44
45 return file_put_contents(
46 $file,
47 self::$phpPrefix.base64_encode(gzdeflate(serialize($content))).self::$phpSuffix
48 );
49 }
50
51 /**
52 * Read data from a file containing Shaarli database format content.
53 * If the file isn't readable or doesn't exists, default data will be returned.
54 *
55 * @param string $file File path.
56 * @param mixed $default The default value to return if the file isn't readable.
57 *
58 * @return mixed The content unserialized, or default if the file isn't readable, or false if it fails.
59 */
60 public static function readFlatDB($file, $default = null)
61 {
62 // Note that gzinflate is faster than gzuncompress.
63 // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
64 if (is_readable($file)) {
65 return unserialize(
66 gzinflate(
67 base64_decode(
68 substr(file_get_contents($file), strlen(self::$phpPrefix), -strlen(self::$phpSuffix))
69 )
70 )
71 );
72 }
73
74 return $default;
20 } 75 }
21} 76}
diff --git a/application/History.php b/application/History.php
new file mode 100644
index 00000000..f93b0356
--- /dev/null
+++ b/application/History.php
@@ -0,0 +1,200 @@
1<?php
2
3/**
4 * Class History
5 *
6 * Handle the history file tracing events in Shaarli.
7 * The history is stored as JSON in a file set by 'resource.history' setting.
8 *
9 * Available data:
10 * - event: event key
11 * - datetime: event date, in ISO8601 format.
12 * - id: event item identifier (currently only link IDs).
13 *
14 * Available event keys:
15 * - CREATED: new link
16 * - UPDATED: link updated
17 * - DELETED: link deleted
18 * - SETTINGS: the settings have been updated through the UI.
19 *
20 * Note: new events are put at the beginning of the file and history array.
21 */
22class History
23{
24 /**
25 * @var string Action key: a new link has been created.
26 */
27 const CREATED = 'CREATED';
28
29 /**
30 * @var string Action key: a link has been updated.
31 */
32 const UPDATED = 'UPDATED';
33
34 /**
35 * @var string Action key: a link has been deleted.
36 */
37 const DELETED = 'DELETED';
38
39 /**
40 * @var string Action key: settings have been updated.
41 */
42 const SETTINGS = 'SETTINGS';
43
44 /**
45 * @var string History file path.
46 */
47 protected $historyFilePath;
48
49 /**
50 * @var array History data.
51 */
52 protected $history;
53
54 /**
55 * @var int History retention time in seconds (1 month).
56 */
57 protected $retentionTime = 2678400;
58
59 /**
60 * History constructor.
61 *
62 * @param string $historyFilePath History file path.
63 * @param int $retentionTime History content rentention time in seconds.
64 *
65 * @throws Exception if something goes wrong.
66 */
67 public function __construct($historyFilePath, $retentionTime = null)
68 {
69 $this->historyFilePath = $historyFilePath;
70 if ($retentionTime !== null) {
71 $this->retentionTime = $retentionTime;
72 }
73 }
74
75 /**
76 * Initialize: read history file.
77 *
78 * Allow lazy loading (don't read the file if it isn't necessary).
79 */
80 protected function initialize()
81 {
82 $this->check();
83 $this->read();
84 }
85
86 /**
87 * Add Event: new link.
88 *
89 * @param array $link Link data.
90 */
91 public function addLink($link)
92 {
93 $this->addEvent(self::CREATED, $link['id']);
94 }
95
96 /**
97 * Add Event: update existing link.
98 *
99 * @param array $link Link data.
100 */
101 public function updateLink($link)
102 {
103 $this->addEvent(self::UPDATED, $link['id']);
104 }
105
106 /**
107 * Add Event: delete existing link.
108 *
109 * @param array $link Link data.
110 */
111 public function deleteLink($link)
112 {
113 $this->addEvent(self::DELETED, $link['id']);
114 }
115
116 /**
117 * Add Event: settings updated.
118 */
119 public function updateSettings()
120 {
121 $this->addEvent(self::SETTINGS);
122 }
123
124 /**
125 * Save a new event and write it in the history file.
126 *
127 * @param string $status Event key, should be defined as constant.
128 * @param mixed $id Event item identifier (e.g. link ID).
129 */
130 protected function addEvent($status, $id = null)
131 {
132 if ($this->history === null) {
133 $this->initialize();
134 }
135
136 $item = [
137 'event' => $status,
138 'datetime' => (new DateTime())->format(DateTime::ATOM),
139 'id' => $id !== null ? $id : '',
140 ];
141 $this->history = array_merge([$item], $this->history);
142 $this->write();
143 }
144
145 /**
146 * Check that the history file is writable.
147 * Create the file if it doesn't exist.
148 *
149 * @throws Exception if it isn't writable.
150 */
151 protected function check()
152 {
153 if (! is_file($this->historyFilePath)) {
154 FileUtils::writeFlatDB($this->historyFilePath, []);
155 }
156
157 if (! is_writable($this->historyFilePath)) {
158 throw new Exception('History file isn\'t readable or writable');
159 }
160 }
161
162 /**
163 * Read JSON history file.
164 */
165 protected function read()
166 {
167 $this->history = FileUtils::readFlatDB($this->historyFilePath, []);
168 if ($this->history === false) {
169 throw new Exception('Could not parse history file');
170 }
171 }
172
173 /**
174 * Write JSON history file and delete old entries.
175 */
176 protected function write()
177 {
178 $comparaison = new DateTime('-'. $this->retentionTime . ' seconds');
179 foreach ($this->history as $key => $value) {
180 if (DateTime::createFromFormat(DateTime::ATOM, $value['datetime']) < $comparaison) {
181 unset($this->history[$key]);
182 }
183 }
184 FileUtils::writeFlatDB($this->historyFilePath, array_values($this->history));
185 }
186
187 /**
188 * Get the History.
189 *
190 * @return array
191 */
192 public function getHistory()
193 {
194 if ($this->history === null) {
195 $this->initialize();
196 }
197
198 return $this->history;
199 }
200}
diff --git a/application/LinkDB.php b/application/LinkDB.php
index 4cee2af9..0d3c85bd 100644
--- a/application/LinkDB.php
+++ b/application/LinkDB.php
@@ -50,12 +50,6 @@ class LinkDB implements Iterator, Countable, ArrayAccess
50 // Link date storage format 50 // Link date storage format
51 const LINK_DATE_FORMAT = 'Ymd_His'; 51 const LINK_DATE_FORMAT = 'Ymd_His';
52 52
53 // Datastore PHP prefix
54 protected static $phpPrefix = '<?php /* ';
55
56 // Datastore PHP suffix
57 protected static $phpSuffix = ' */ ?>';
58
59 // List of links (associative array) 53 // List of links (associative array)
60 // - key: link date (e.g. "20110823_124546"), 54 // - key: link date (e.g. "20110823_124546"),
61 // - value: associative array (keys: title, description...) 55 // - value: associative array (keys: title, description...)
@@ -144,10 +138,10 @@ class LinkDB implements Iterator, Countable, ArrayAccess
144 if (!isset($value['id']) || empty($value['url'])) { 138 if (!isset($value['id']) || empty($value['url'])) {
145 die('Internal Error: A link should always have an id and URL.'); 139 die('Internal Error: A link should always have an id and URL.');
146 } 140 }
147 if ((! empty($offset) && ! is_int($offset)) || ! is_int($value['id'])) { 141 if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) {
148 die('You must specify an integer as a key.'); 142 die('You must specify an integer as a key.');
149 } 143 }
150 if (! empty($offset) && $offset !== $value['id']) { 144 if ($offset !== null && $offset !== $value['id']) {
151 die('Array offset and link ID must be equal.'); 145 die('Array offset and link ID must be equal.');
152 } 146 }
153 147
@@ -295,16 +289,7 @@ You use the community supported version of the original Shaarli project, by Seba
295 return; 289 return;
296 } 290 }
297 291
298 // Read data 292 $this->links = FileUtils::readFlatDB($this->datastore, []);
299 // Note that gzinflate is faster than gzuncompress.
300 // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
301 $this->links = array();
302
303 if (file_exists($this->datastore)) {
304 $this->links = unserialize(gzinflate(base64_decode(
305 substr(file_get_contents($this->datastore),
306 strlen(self::$phpPrefix), -strlen(self::$phpSuffix)))));
307 }
308 293
309 $toremove = array(); 294 $toremove = array();
310 foreach ($this->links as $key => &$link) { 295 foreach ($this->links as $key => &$link) {
@@ -361,19 +346,7 @@ You use the community supported version of the original Shaarli project, by Seba
361 */ 346 */
362 private function write() 347 private function write()
363 { 348 {
364 if (is_file($this->datastore) && !is_writeable($this->datastore)) { 349 FileUtils::writeFlatDB($this->datastore, $this->links);
365 // The datastore exists but is not writeable
366 throw new IOException($this->datastore);
367 } else if (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) {
368 // The datastore does not exist and its parent directory is not writeable
369 throw new IOException(dirname($this->datastore));
370 }
371
372 file_put_contents(
373 $this->datastore,
374 self::$phpPrefix.base64_encode(gzdeflate(serialize($this->links))).self::$phpSuffix
375 );
376
377 } 350 }
378 351
379 /** 352 /**
diff --git a/application/NetscapeBookmarkUtils.php b/application/NetscapeBookmarkUtils.php
index ab346f81..bbfde138 100644
--- a/application/NetscapeBookmarkUtils.php
+++ b/application/NetscapeBookmarkUtils.php
@@ -95,10 +95,11 @@ class NetscapeBookmarkUtils
95 * @param array $files Server $_FILES parameters 95 * @param array $files Server $_FILES parameters
96 * @param LinkDB $linkDb Loaded LinkDB instance 96 * @param LinkDB $linkDb Loaded LinkDB instance
97 * @param ConfigManager $conf instance 97 * @param ConfigManager $conf instance
98 * @param History $history History instance
98 * 99 *
99 * @return string Summary of the bookmark import status 100 * @return string Summary of the bookmark import status
100 */ 101 */
101 public static function import($post, $files, $linkDb, $conf) 102 public static function import($post, $files, $linkDb, $conf, $history)
102 { 103 {
103 $filename = $files['filetoupload']['name']; 104 $filename = $files['filetoupload']['name'];
104 $filesize = $files['filetoupload']['size']; 105 $filesize = $files['filetoupload']['size'];
@@ -182,6 +183,7 @@ class NetscapeBookmarkUtils
182 $linkDb[$existingLink['id']] = $newLink; 183 $linkDb[$existingLink['id']] = $newLink;
183 $importCount++; 184 $importCount++;
184 $overwriteCount++; 185 $overwriteCount++;
186 $history->updateLink($newLink);
185 continue; 187 continue;
186 } 188 }
187 189
@@ -193,6 +195,7 @@ class NetscapeBookmarkUtils
193 $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']); 195 $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']);
194 $linkDb[$newLink['id']] = $newLink; 196 $linkDb[$newLink['id']] = $newLink;
195 $importCount++; 197 $importCount++;
198 $history->addLink($newLink);
196 } 199 }
197 200
198 $linkDb->save($conf->get('resource.page_cache')); 201 $linkDb->save($conf->get('resource.page_cache'));
diff --git a/application/PageBuilder.php b/application/PageBuilder.php
index b133dee8..8e39455b 100644
--- a/application/PageBuilder.php
+++ b/application/PageBuilder.php
@@ -1,5 +1,7 @@
1<?php 1<?php
2 2
3use Shaarli\Config\ConfigManager;
4
3/** 5/**
4 * This class is in charge of building the final page. 6 * This class is in charge of building the final page.
5 * (This is basically a wrapper around RainTPL which pre-fills some fields.) 7 * (This is basically a wrapper around RainTPL which pre-fills some fields.)
diff --git a/application/TimeZone.php b/application/TimeZone.php
index 36a8fb12..c1869ef8 100644
--- a/application/TimeZone.php
+++ b/application/TimeZone.php
@@ -1,23 +1,42 @@
1<?php 1<?php
2/** 2/**
3 * Generates the timezone selection form and JavaScript. 3 * Generates a list of available timezone continents and cities.
4 * 4 *
5 * Note: 'UTC/UTC' is mapped to 'UTC' to form a valid option 5 * Two distinct array based on available timezones
6 * and the one selected in the settings:
7 * - (0) continents:
8 * + list of available continents
9 * + special key 'selected' containing the value of the selected timezone's continent
10 * - (1) cities:
11 * + list of available cities associated with their continent
12 * + special key 'selected' containing the value of the selected timezone's city (without the continent)
6 * 13 *
7 * Example: preselect Europe/Paris 14 * Example:
8 * list($htmlform, $js) = generateTimeZoneForm('Europe/Paris'); 15 * [
16 * [
17 * 'America',
18 * 'Europe',
19 * 'selected' => 'Europe',
20 * ],
21 * [
22 * ['continent' => 'America', 'city' => 'Toronto'],
23 * ['continent' => 'Europe', 'city' => 'Paris'],
24 * 'selected' => 'Paris',
25 * ],
26 * ];
9 * 27 *
28 * Notes:
29 * - 'UTC/UTC' is mapped to 'UTC' to form a valid option
30 * - a few timezone cities includes the country/state, such as Argentina/Buenos_Aires
31 * - these arrays are designed to build timezone selects in template files with any HTML structure
32 *
33 * @param array $installedTimeZones List of installed timezones as string
10 * @param string $preselectedTimezone preselected timezone (optional) 34 * @param string $preselectedTimezone preselected timezone (optional)
11 * 35 *
12 * @return array containing the generated HTML form and Javascript code 36 * @return array[] continents and cities
13 **/ 37 **/
14function generateTimeZoneForm($preselectedTimezone='') 38function generateTimeZoneData($installedTimeZones, $preselectedTimezone = '')
15{ 39{
16 // Select the server timezone
17 if ($preselectedTimezone == '') {
18 $preselectedTimezone = date_default_timezone_get();
19 }
20
21 if ($preselectedTimezone == 'UTC') { 40 if ($preselectedTimezone == 'UTC') {
22 $pcity = $pcontinent = 'UTC'; 41 $pcity = $pcontinent = 'UTC';
23 } else { 42 } else {
@@ -27,62 +46,30 @@ function generateTimeZoneForm($preselectedTimezone='')
27 $pcity = substr($preselectedTimezone, $spos+1); 46 $pcity = substr($preselectedTimezone, $spos+1);
28 } 47 }
29 48
30 // The list is in the form 'Europe/Paris', 'America/Argentina/Buenos_Aires' 49 $continents = [];
31 // We split the list in continents/cities. 50 $cities = [];
32 $continents = array(); 51 foreach ($installedTimeZones as $tz) {
33 $cities = array();
34
35 // TODO: use a template to generate the HTML/Javascript form
36
37 foreach (timezone_identifiers_list() as $tz) {
38 if ($tz == 'UTC') { 52 if ($tz == 'UTC') {
39 $tz = 'UTC/UTC'; 53 $tz = 'UTC/UTC';
40 } 54 }
41 $spos = strpos($tz, '/'); 55 $spos = strpos($tz, '/');
42 56
43 if ($spos !== false) { 57 // Ignore invalid timezones
44 $continent = substr($tz, 0, $spos); 58 if ($spos === false) {
45 $city = substr($tz, $spos+1); 59 continue;
46 $continents[$continent] = 1;
47
48 if (!isset($cities[$continent])) {
49 $cities[$continent] = '';
50 }
51 $cities[$continent] .= '<option value="'.$city.'"';
52 if ($pcity == $city) {
53 $cities[$continent] .= ' selected="selected"';
54 }
55 $cities[$continent] .= '>'.$city.'</option>';
56 } 60 }
57 }
58
59 $continentsHtml = '';
60 $continents = array_keys($continents);
61 61
62 foreach ($continents as $continent) { 62 $continent = substr($tz, 0, $spos);
63 $continentsHtml .= '<option value="'.$continent.'"'; 63 $city = substr($tz, $spos+1);
64 if ($pcontinent == $continent) { 64 $cities[] = ['continent' => $continent, 'city' => $city];
65 $continentsHtml .= ' selected="selected"'; 65 $continents[$continent] = true;
66 }
67 $continentsHtml .= '>'.$continent.'</option>';
68 } 66 }
69 67
70 // Timezone selection form 68 $continents = array_keys($continents);
71 $timezoneForm = 'Continent:'; 69 $continents['selected'] = $pcontinent;
72 $timezoneForm .= '<select name="continent" id="continent" onChange="onChangecontinent();">'; 70 $cities['selected'] = $pcity;
73 $timezoneForm .= $continentsHtml.'</select>';
74 $timezoneForm .= '&nbsp;&nbsp;&nbsp;&nbsp;City:';
75 $timezoneForm .= '<select name="city" id="city">'.$cities[$pcontinent].'</select><br />';
76
77 // Javascript handler - updates the city list when the user selects a continent
78 $timezoneJs = '<script>';
79 $timezoneJs .= 'function onChangecontinent() {';
80 $timezoneJs .= 'document.getElementById("city").innerHTML =';
81 $timezoneJs .= ' citiescontinent[document.getElementById("continent").value]; }';
82 $timezoneJs .= 'var citiescontinent = '.json_encode($cities).';';
83 $timezoneJs .= '</script>';
84 71
85 return array($timezoneForm, $timezoneJs); 72 return [$continents, $cities];
86} 73}
87 74
88/** 75/**
diff --git a/application/Utils.php b/application/Utils.php
index d6e06610..ab463af9 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -345,3 +345,93 @@ function format_date($date, $time = true, $intl = true)
345 345
346 return $formatter->format($date); 346 return $formatter->format($date);
347} 347}
348
349/**
350 * Check if the input is an integer, no matter its real type.
351 *
352 * PHP is a bit messy regarding this:
353 * - is_int returns false if the input is a string
354 * - ctype_digit returns false if the input is an integer or negative
355 *
356 * @param mixed $input value
357 *
358 * @return bool true if the input is an integer, false otherwise
359 */
360function is_integer_mixed($input)
361{
362 if (is_array($input) || is_bool($input) || is_object($input)) {
363 return false;
364 }
365 $input = strval($input);
366 return ctype_digit($input) || (startsWith($input, '-') && ctype_digit(substr($input, 1)));
367}
368
369/**
370 * Convert post_max_size/upload_max_filesize (e.g. '16M') parameters to bytes.
371 *
372 * @param string $val Size expressed in string.
373 *
374 * @return int Size expressed in bytes.
375 */
376function return_bytes($val)
377{
378 if (is_integer_mixed($val) || $val === '0' || empty($val)) {
379 return $val;
380 }
381 $val = trim($val);
382 $last = strtolower($val[strlen($val)-1]);
383 $val = intval(substr($val, 0, -1));
384 switch($last) {
385 case 'g': $val *= 1024;
386 case 'm': $val *= 1024;
387 case 'k': $val *= 1024;
388 }
389 return $val;
390}
391
392/**
393 * Return a human readable size from bytes.
394 *
395 * @param int $bytes value
396 *
397 * @return string Human readable size
398 */
399function human_bytes($bytes)
400{
401 if ($bytes === '') {
402 return t('Setting not set');
403 }
404 if (! is_integer_mixed($bytes)) {
405 return $bytes;
406 }
407 $bytes = intval($bytes);
408 if ($bytes === 0) {
409 return t('Unlimited');
410 }
411
412 $units = [t('B'), t('kiB'), t('MiB'), t('GiB')];
413 for ($i = 0; $i < count($units) && $bytes >= 1024; ++$i) {
414 $bytes /= 1024;
415 }
416
417 return round($bytes) . $units[$i];
418}
419
420/**
421 * Try to determine max file size for uploads (POST).
422 * Returns an integer (in bytes) or formatted depending on $format.
423 *
424 * @param mixed $limitPost post_max_size PHP setting
425 * @param mixed $limitUpload upload_max_filesize PHP setting
426 * @param bool $format Format max upload size to human readable size
427 *
428 * @return int|string max upload file size
429 */
430function get_max_upload_size($limitPost, $limitUpload, $format = true)
431{
432 $size1 = return_bytes($limitPost);
433 $size2 = return_bytes($limitUpload);
434 // Return the smaller of two:
435 $maxsize = min($size1, $size2);
436 return $format ? human_bytes($maxsize) : $maxsize;
437}
diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php
index 7bfbfc72..86a917fb 100644
--- a/application/config/ConfigManager.php
+++ b/application/config/ConfigManager.php
@@ -301,6 +301,7 @@ class ConfigManager
301 $this->setEmpty('resource.updates', 'data/updates.txt'); 301 $this->setEmpty('resource.updates', 'data/updates.txt');
302 $this->setEmpty('resource.log', 'data/log.txt'); 302 $this->setEmpty('resource.log', 'data/log.txt');
303 $this->setEmpty('resource.update_check', 'data/lastupdatecheck.txt'); 303 $this->setEmpty('resource.update_check', 'data/lastupdatecheck.txt');
304 $this->setEmpty('resource.history', 'data/history.php');
304 $this->setEmpty('resource.raintpl_tpl', 'tpl/'); 305 $this->setEmpty('resource.raintpl_tpl', 'tpl/');
305 $this->setEmpty('resource.theme', 'default'); 306 $this->setEmpty('resource.theme', 'default');
306 $this->setEmpty('resource.raintpl_tmp', 'tmp/'); 307 $this->setEmpty('resource.raintpl_tmp', 'tmp/');
diff --git a/application/exceptions/IOException.php b/application/exceptions/IOException.php
new file mode 100644
index 00000000..b563b23d
--- /dev/null
+++ b/application/exceptions/IOException.php
@@ -0,0 +1,22 @@
1<?php
2
3/**
4 * Exception class thrown when a filesystem access failure happens
5 */
6class IOException extends Exception
7{
8 private $path;
9
10 /**
11 * Construct a new IOException
12 *
13 * @param string $path path to the resource that cannot be accessed
14 * @param string $message Custom exception message.
15 */
16 public function __construct($path, $message = '')
17 {
18 $this->path = $path;
19 $this->message = empty($message) ? 'Error accessing' : $message;
20 $this->message .= ' "' . $this->path .'"';
21 }
22}
diff --git a/index.php b/index.php
index 6eaa56c2..02fe2577 100644
--- a/index.php
+++ b/index.php
@@ -62,6 +62,7 @@ require_once 'application/CachedPage.php';
62require_once 'application/config/ConfigPlugin.php'; 62require_once 'application/config/ConfigPlugin.php';
63require_once 'application/FeedBuilder.php'; 63require_once 'application/FeedBuilder.php';
64require_once 'application/FileUtils.php'; 64require_once 'application/FileUtils.php';
65require_once 'application/History.php';
65require_once 'application/HttpUtils.php'; 66require_once 'application/HttpUtils.php';
66require_once 'application/Languages.php'; 67require_once 'application/Languages.php';
67require_once 'application/LinkDB.php'; 68require_once 'application/LinkDB.php';
@@ -473,34 +474,6 @@ if (isset($_POST['login']))
473} 474}
474 475
475// ------------------------------------------------------------------------------------------ 476// ------------------------------------------------------------------------------------------
476// Misc utility functions:
477
478// Convert post_max_size/upload_max_filesize (e.g. '16M') parameters to bytes.
479function return_bytes($val)
480{
481 $val = trim($val); $last=strtolower($val[strlen($val)-1]);
482 switch($last)
483 {
484 case 'g': $val *= 1024;
485 case 'm': $val *= 1024;
486 case 'k': $val *= 1024;
487 }
488 return $val;
489}
490
491// Try to determine max file size for uploads (POST).
492// Returns an integer (in bytes)
493function getMaxFileSize()
494{
495 $size1 = return_bytes(ini_get('post_max_size'));
496 $size2 = return_bytes(ini_get('upload_max_filesize'));
497 // Return the smaller of two:
498 $maxsize = min($size1,$size2);
499 // FIXME: Then convert back to readable notations ? (e.g. 2M instead of 2000000)
500 return $maxsize;
501}
502
503// ------------------------------------------------------------------------------------------
504// Token management for XSRF protection 477// Token management for XSRF protection
505// Token should be used in any form which acts on data (create,update,delete,import...). 478// Token should be used in any form which acts on data (create,update,delete,import...).
506if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are attached to the session. 479if (!isset($_SESSION['tokens'])) $_SESSION['tokens']=array(); // Token are attached to the session.
@@ -755,6 +728,12 @@ function renderPage($conf, $pluginManager, $LINKSDB)
755 die($e->getMessage()); 728 die($e->getMessage());
756 } 729 }
757 730
731 try {
732 $history = new History($conf->get('resource.history'));
733 } catch(Exception $e) {
734 die($e->getMessage());
735 }
736
758 $PAGE = new PageBuilder($conf); 737 $PAGE = new PageBuilder($conf);
759 $PAGE->assign('linkcount', count($LINKSDB)); 738 $PAGE->assign('linkcount', count($LINKSDB));
760 $PAGE->assign('privateLinkcount', count_private($LINKSDB)); 739 $PAGE->assign('privateLinkcount', count_private($LINKSDB));
@@ -1153,6 +1132,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1153 $conf->set('api.secret', escape($_POST['apiSecret'])); 1132 $conf->set('api.secret', escape($_POST['apiSecret']));
1154 try { 1133 try {
1155 $conf->write(isLoggedIn()); 1134 $conf->write(isLoggedIn());
1135 $history->updateSettings();
1156 invalidateCaches($conf->get('resource.page_cache')); 1136 invalidateCaches($conf->get('resource.page_cache'));
1157 } 1137 }
1158 catch(Exception $e) { 1138 catch(Exception $e) {
@@ -1174,9 +1154,12 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1174 $PAGE->assign('theme', $conf->get('resource.theme')); 1154 $PAGE->assign('theme', $conf->get('resource.theme'));
1175 $PAGE->assign('theme_available', ThemeUtils::getThemes($conf->get('resource.raintpl_tpl'))); 1155 $PAGE->assign('theme_available', ThemeUtils::getThemes($conf->get('resource.raintpl_tpl')));
1176 $PAGE->assign('redirector', $conf->get('redirector.url')); 1156 $PAGE->assign('redirector', $conf->get('redirector.url'));
1177 list($timezone_form, $timezone_js) = generateTimeZoneForm($conf->get('general.timezone')); 1157 list($continents, $cities) = generateTimeZoneData(
1178 $PAGE->assign('timezone_form', $timezone_form); 1158 timezone_identifiers_list(),
1179 $PAGE->assign('timezone_js',$timezone_js); 1159 $conf->get('general.timezone')
1160 );
1161 $PAGE->assign('continents', $continents);
1162 $PAGE->assign('cities', $cities);
1180 $PAGE->assign('private_links_default', $conf->get('privacy.default_private_links', false)); 1163 $PAGE->assign('private_links_default', $conf->get('privacy.default_private_links', false));
1181 $PAGE->assign('session_protection_disabled', $conf->get('security.session_protection_disabled', false)); 1164 $PAGE->assign('session_protection_disabled', $conf->get('security.session_protection_disabled', false));
1182 $PAGE->assign('enable_rss_permalinks', $conf->get('feed.rss_permalinks', false)); 1165 $PAGE->assign('enable_rss_permalinks', $conf->get('feed.rss_permalinks', false));
@@ -1184,6 +1167,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1184 $PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false)); 1167 $PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false));
1185 $PAGE->assign('api_enabled', $conf->get('api.enabled', true)); 1168 $PAGE->assign('api_enabled', $conf->get('api.enabled', true));
1186 $PAGE->assign('api_secret', $conf->get('api.secret')); 1169 $PAGE->assign('api_secret', $conf->get('api.secret'));
1170 $history->updateSettings();
1187 $PAGE->renderPage('configure'); 1171 $PAGE->renderPage('configure');
1188 exit; 1172 exit;
1189 } 1173 }
@@ -1213,6 +1197,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1213 unset($tags[array_search($needle,$tags)]); // Remove tag. 1197 unset($tags[array_search($needle,$tags)]); // Remove tag.
1214 $value['tags']=trim(implode(' ',$tags)); 1198 $value['tags']=trim(implode(' ',$tags));
1215 $LINKSDB[$key]=$value; 1199 $LINKSDB[$key]=$value;
1200 $history->updateLink($LINKSDB[$key]);
1216 } 1201 }
1217 $LINKSDB->save($conf->get('resource.page_cache')); 1202 $LINKSDB->save($conf->get('resource.page_cache'));
1218 echo '<script>alert("Tag was removed from '.count($linksToAlter).' links.");document.location=\'?do=changetag\';</script>'; 1203 echo '<script>alert("Tag was removed from '.count($linksToAlter).' links.");document.location=\'?do=changetag\';</script>';
@@ -1230,6 +1215,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1230 $tags[array_search($needle, $tags)] = trim($_POST['totag']); 1215 $tags[array_search($needle, $tags)] = trim($_POST['totag']);
1231 $value['tags'] = implode(' ', array_unique($tags)); 1216 $value['tags'] = implode(' ', array_unique($tags));
1232 $LINKSDB[$key] = $value; 1217 $LINKSDB[$key] = $value;
1218 $history->updateLink($LINKSDB[$key]);
1233 } 1219 }
1234 $LINKSDB->save($conf->get('resource.page_cache')); // Save to disk. 1220 $LINKSDB->save($conf->get('resource.page_cache')); // Save to disk.
1235 echo '<script>alert("Tag was renamed in '.count($linksToAlter).' links.");document.location=\'?searchtags='.urlencode(escape($_POST['totag'])).'\';</script>'; 1221 echo '<script>alert("Tag was renamed in '.count($linksToAlter).' links.");document.location=\'?searchtags='.urlencode(escape($_POST['totag'])).'\';</script>';
@@ -1264,11 +1250,13 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1264 $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate); 1250 $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate);
1265 $updated = new DateTime(); 1251 $updated = new DateTime();
1266 $shortUrl = $LINKSDB[$id]['shorturl']; 1252 $shortUrl = $LINKSDB[$id]['shorturl'];
1253 $new = false;
1267 } else { 1254 } else {
1268 // New link 1255 // New link
1269 $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate); 1256 $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate);
1270 $updated = null; 1257 $updated = null;
1271 $shortUrl = link_small_hash($created, $id); 1258 $shortUrl = link_small_hash($created, $id);
1259 $new = true;
1272 } 1260 }
1273 1261
1274 // Remove multiple spaces. 1262 // Remove multiple spaces.
@@ -1307,6 +1295,11 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1307 1295
1308 $LINKSDB[$id] = $link; 1296 $LINKSDB[$id] = $link;
1309 $LINKSDB->save($conf->get('resource.page_cache')); 1297 $LINKSDB->save($conf->get('resource.page_cache'));
1298 if ($new) {
1299 $history->addLink($link);
1300 } else {
1301 $history->updateLink($link);
1302 }
1310 1303
1311 // If we are called from the bookmarklet, we must close the popup: 1304 // If we are called from the bookmarklet, we must close the popup:
1312 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { 1305 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
@@ -1357,6 +1350,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1357 $pluginManager->executeHooks('delete_link', $link); 1350 $pluginManager->executeHooks('delete_link', $link);
1358 unset($LINKSDB[$id]); 1351 unset($LINKSDB[$id]);
1359 $LINKSDB->save($conf->get('resource.page_cache')); // save to disk 1352 $LINKSDB->save($conf->get('resource.page_cache')); // save to disk
1353 $history->deleteLink($link);
1360 1354
1361 // If we are called from the bookmarklet, we must close the popup: 1355 // If we are called from the bookmarklet, we must close the popup:
1362 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo '<script>self.close();</script>'; exit; } 1356 if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo '<script>self.close();</script>'; exit; }
@@ -1517,7 +1511,22 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1517 1511
1518 if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) { 1512 if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) {
1519 // Show import dialog 1513 // Show import dialog
1520 $PAGE->assign('maxfilesize', getMaxFileSize()); 1514 $PAGE->assign(
1515 'maxfilesize',
1516 get_max_upload_size(
1517 ini_get('post_max_size'),
1518 ini_get('upload_max_filesize'),
1519 false
1520 )
1521 );
1522 $PAGE->assign(
1523 'maxfilesizeHuman',
1524 get_max_upload_size(
1525 ini_get('post_max_size'),
1526 ini_get('upload_max_filesize'),
1527 true
1528 )
1529 );
1521 $PAGE->renderPage('import'); 1530 $PAGE->renderPage('import');
1522 exit; 1531 exit;
1523 } 1532 }
@@ -1527,7 +1536,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1527 // The file is too big or some form field may be missing. 1536 // The file is too big or some form field may be missing.
1528 echo '<script>alert("The file you are trying to upload is probably' 1537 echo '<script>alert("The file you are trying to upload is probably'
1529 .' bigger than what this webserver can accept (' 1538 .' bigger than what this webserver can accept ('
1530 .getMaxFileSize().' bytes).' 1539 .get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')).').'
1531 .' Please upload in smaller chunks.");document.location=\'?do=' 1540 .' Please upload in smaller chunks.");document.location=\'?do='
1532 .Router::$PAGE_IMPORT .'\';</script>'; 1541 .Router::$PAGE_IMPORT .'\';</script>';
1533 exit; 1542 exit;
@@ -1539,7 +1548,8 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1539 $_POST, 1548 $_POST,
1540 $_FILES, 1549 $_FILES,
1541 $LINKSDB, 1550 $LINKSDB,
1542 $conf 1551 $conf,
1552 $history
1543 ); 1553 );
1544 echo '<script>alert("'.$status.'");document.location=\'?do=' 1554 echo '<script>alert("'.$status.'");document.location=\'?do='
1545 .Router::$PAGE_IMPORT .'\';</script>'; 1555 .Router::$PAGE_IMPORT .'\';</script>';
@@ -1568,6 +1578,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
1568 1578
1569 // Plugin administration form action 1579 // Plugin administration form action
1570 if ($targetPage == Router::$PAGE_SAVE_PLUGINSADMIN) { 1580 if ($targetPage == Router::$PAGE_SAVE_PLUGINSADMIN) {
1581 $history->updateSettings();
1571 try { 1582 try {
1572 if (isset($_POST['parameters_form'])) { 1583 if (isset($_POST['parameters_form'])) {
1573 unset($_POST['parameters_form']); 1584 unset($_POST['parameters_form']);
@@ -1982,16 +1993,10 @@ function install($conf)
1982 exit; 1993 exit;
1983 } 1994 }
1984 1995
1985 // Display config form:
1986 list($timezone_form, $timezone_js) = generateTimeZoneForm();
1987 $timezone_html = '';
1988 if ($timezone_form != '') {
1989 $timezone_html = '<tr><td><b>Timezone:</b></td><td>'.$timezone_form.'</td></tr>';
1990 }
1991
1992 $PAGE = new PageBuilder($conf); 1996 $PAGE = new PageBuilder($conf);
1993 $PAGE->assign('timezone_html',$timezone_html); 1997 list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
1994 $PAGE->assign('timezone_js',$timezone_js); 1998 $PAGE->assign('continents', $continents);
1999 $PAGE->assign('cities', $cities);
1995 $PAGE->renderPage('install'); 2000 $PAGE->renderPage('install');
1996 exit; 2001 exit;
1997} 2002}
diff --git a/plugins/readityourself/book-open.png b/plugins/readityourself/book-open.png
deleted file mode 100644
index 36513d7b..00000000
--- a/plugins/readityourself/book-open.png
+++ /dev/null
Binary files differ
diff --git a/plugins/readityourself/readityourself.html b/plugins/readityourself/readityourself.html
deleted file mode 100644
index 5e200715..00000000
--- a/plugins/readityourself/readityourself.html
+++ /dev/null
@@ -1 +0,0 @@
1<span><a href="%s?url=%s"><img class="linklist-plugin-icon" src="%s/readityourself/book-open.png" title="Read with Readityourself" alt="readityourself" /></a></span>
diff --git a/plugins/readityourself/readityourself.meta b/plugins/readityourself/readityourself.meta
deleted file mode 100644
index bd611dd0..00000000
--- a/plugins/readityourself/readityourself.meta
+++ /dev/null
@@ -1,2 +0,0 @@
1description="For each link, add a ReadItYourself icon to save the shaared URL."
2parameters=READITYOUSELF_URL; \ No newline at end of file
diff --git a/plugins/readityourself/readityourself.php b/plugins/readityourself/readityourself.php
deleted file mode 100644
index 961c5bda..00000000
--- a/plugins/readityourself/readityourself.php
+++ /dev/null
@@ -1,51 +0,0 @@
1<?php
2
3/**
4 * Plugin readityourself
5 */
6
7// If we're talking about https://github.com/memiks/readityourself
8// it seems kinda dead.
9// Not tested.
10
11/**
12 * Init function, return an error if the server is not set.
13 *
14 * @param $conf ConfigManager instance.
15 *
16 * @return array Eventual error.
17 */
18function readityourself_init($conf)
19{
20 $riyUrl = $conf->get('plugins.READITYOUSELF_URL');
21 if (empty($riyUrl)) {
22 $error = 'Readityourself plugin error: '.
23 'Please define the "READITYOUSELF_URL" setting in the plugin administration page.';
24 return array($error);
25 }
26}
27
28/**
29 * Add readityourself icon to link_plugin when rendering linklist.
30 *
31 * @param mixed $data Linklist data.
32 * @param ConfigManager $conf Configuration Manager instance.
33 *
34 * @return mixed - linklist data with readityourself plugin.
35 */
36function hook_readityourself_render_linklist($data, $conf)
37{
38 $riyUrl = $conf->get('plugins.READITYOUSELF_URL');
39 if (empty($riyUrl)) {
40 return $data;
41 }
42
43 $readityourself_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/readityourself/readityourself.html');
44
45 foreach ($data['links'] as &$value) {
46 $readityourself = sprintf($readityourself_html, $riyUrl, $value['url'], PluginManager::$PLUGINS_PATH);
47 $value['link_plugin'][] = $readityourself;
48 }
49
50 return $data;
51}
diff --git a/tests/FileUtilsTest.php b/tests/FileUtilsTest.php
new file mode 100644
index 00000000..d764e495
--- /dev/null
+++ b/tests/FileUtilsTest.php
@@ -0,0 +1,108 @@
1<?php
2
3require_once 'application/FileUtils.php';
4
5/**
6 * Class FileUtilsTest
7 *
8 * Test file utility class.
9 */
10class FileUtilsTest extends PHPUnit_Framework_TestCase
11{
12 /**
13 * @var string Test file path.
14 */
15 protected static $file = 'sandbox/flat.db';
16
17 /**
18 * Delete test file after every test.
19 */
20 public function tearDown()
21 {
22 @unlink(self::$file);
23 }
24
25 /**
26 * Test writeDB, then readDB with different data.
27 */
28 public function testSimpleWriteRead()
29 {
30 $data = ['blue', 'red'];
31 $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0);
32 $this->assertTrue(startsWith(file_get_contents(self::$file), '<?php /*'));
33 $this->assertEquals($data, FileUtils::readFlatDB(self::$file));
34
35 $data = 0;
36 $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0);
37 $this->assertEquals($data, FileUtils::readFlatDB(self::$file));
38
39 $data = null;
40 $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0);
41 $this->assertEquals($data, FileUtils::readFlatDB(self::$file));
42
43 $data = false;
44 $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0);
45 $this->assertEquals($data, FileUtils::readFlatDB(self::$file));
46 }
47
48 /**
49 * File not writable: raise an exception.
50 *
51 * @expectedException IOException
52 * @expectedExceptionMessage Error accessing "sandbox/flat.db"
53 */
54 public function testWriteWithoutPermission()
55 {
56 touch(self::$file);
57 chmod(self::$file, 0440);
58 FileUtils::writeFlatDB(self::$file, null);
59 }
60
61 /**
62 * Folder non existent: raise an exception.
63 *
64 * @expectedException IOException
65 * @expectedExceptionMessage Error accessing "nopefolder"
66 */
67 public function testWriteFolderDoesNotExist()
68 {
69 FileUtils::writeFlatDB('nopefolder/file', null);
70 }
71
72 /**
73 * Folder non writable: raise an exception.
74 *
75 * @expectedException IOException
76 * @expectedExceptionMessage Error accessing "sandbox"
77 */
78 public function testWriteFolderPermission()
79 {
80 chmod(dirname(self::$file), 0555);
81 try {
82 FileUtils::writeFlatDB(self::$file, null);
83 } catch (Exception $e) {
84 chmod(dirname(self::$file), 0755);
85 throw $e;
86 }
87 }
88
89 /**
90 * Read non existent file, use default parameter.
91 */
92 public function testReadNotExistentFile()
93 {
94 $this->assertEquals(null, FileUtils::readFlatDB(self::$file));
95 $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test']));
96 }
97
98 /**
99 * Read non readable file, use default parameter.
100 */
101 public function testReadNotReadable()
102 {
103 touch(self::$file);
104 chmod(self::$file, 0220);
105 $this->assertEquals(null, FileUtils::readFlatDB(self::$file));
106 $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test']));
107 }
108}
diff --git a/tests/HistoryTest.php b/tests/HistoryTest.php
new file mode 100644
index 00000000..91525845
--- /dev/null
+++ b/tests/HistoryTest.php
@@ -0,0 +1,207 @@
1<?php
2
3require_once 'application/History.php';
4
5
6class HistoryTest extends PHPUnit_Framework_TestCase
7{
8 /**
9 * @var string History file path
10 */
11 protected static $historyFilePath = 'sandbox/history.php';
12
13 /**
14 * Delete history file.
15 */
16 public function tearDown()
17 {
18 @unlink(self::$historyFilePath);
19 }
20
21 /**
22 * Test that the history file is created if it doesn't exist.
23 */
24 public function testConstructLazyLoading()
25 {
26 new History(self::$historyFilePath);
27 $this->assertFileNotExists(self::$historyFilePath);
28 }
29
30 /**
31 * Test that the history file is created if it doesn't exist.
32 */
33 public function testAddEventCreateFile()
34 {
35 $history = new History(self::$historyFilePath);
36 $history->updateSettings();
37 $this->assertFileExists(self::$historyFilePath);
38 }
39
40 /**
41 * Not writable history file: raise an exception.
42 *
43 * @expectedException Exception
44 * @expectedExceptionMessage History file isn't readable or writable
45 */
46 public function testConstructNotWritable()
47 {
48 touch(self::$historyFilePath);
49 chmod(self::$historyFilePath, 0440);
50 $history = new History(self::$historyFilePath);
51 $history->updateSettings();
52 }
53
54 /**
55 * Not parsable history file: raise an exception.
56 *
57 * @expectedException Exception
58 * @expectedExceptionMessageRegExp /Could not parse history file/
59 */
60 public function testConstructNotParsable()
61 {
62 file_put_contents(self::$historyFilePath, 'not parsable');
63 $history = new History(self::$historyFilePath);
64 // gzinflate generates a warning
65 @$history->updateSettings();
66 }
67
68 /**
69 * Test add link event
70 */
71 public function testAddLink()
72 {
73 $history = new History(self::$historyFilePath);
74 $history->addLink(['id' => 0]);
75 $actual = $history->getHistory()[0];
76 $this->assertEquals(History::CREATED, $actual['event']);
77 $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
78 $this->assertEquals(0, $actual['id']);
79
80 $history = new History(self::$historyFilePath);
81 $history->addLink(['id' => 1]);
82 $actual = $history->getHistory()[0];
83 $this->assertEquals(History::CREATED, $actual['event']);
84 $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
85 $this->assertEquals(1, $actual['id']);
86
87 $history = new History(self::$historyFilePath);
88 $history->addLink(['id' => 'str']);
89 $actual = $history->getHistory()[0];
90 $this->assertEquals(History::CREATED, $actual['event']);
91 $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
92 $this->assertEquals('str', $actual['id']);
93 }
94
95 /**
96 * Test updated link event
97 */
98 public function testUpdateLink()
99 {
100 $history = new History(self::$historyFilePath);
101 $history->updateLink(['id' => 1]);
102 $actual = $history->getHistory()[0];
103 $this->assertEquals(History::UPDATED, $actual['event']);
104 $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
105 $this->assertEquals(1, $actual['id']);
106 }
107
108 /**
109 * Test delete link event
110 */
111 public function testDeleteLink()
112 {
113 $history = new History(self::$historyFilePath);
114 $history->deleteLink(['id' => 1]);
115 $actual = $history->getHistory()[0];
116 $this->assertEquals(History::DELETED, $actual['event']);
117 $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
118 $this->assertEquals(1, $actual['id']);
119 }
120
121 /**
122 * Test updated settings event
123 */
124 public function testUpdateSettings()
125 {
126 $history = new History(self::$historyFilePath);
127 $history->updateSettings();
128 $actual = $history->getHistory()[0];
129 $this->assertEquals(History::SETTINGS, $actual['event']);
130 $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
131 $this->assertEmpty($actual['id']);
132 }
133
134 /**
135 * Make sure that new items are stored at the beginning
136 */
137 public function testHistoryOrder()
138 {
139 $history = new History(self::$historyFilePath);
140 $history->updateLink(['id' => 1]);
141 $actual = $history->getHistory()[0];
142 $this->assertEquals(History::UPDATED, $actual['event']);
143 $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
144 $this->assertEquals(1, $actual['id']);
145
146 $history->addLink(['id' => 1]);
147 $actual = $history->getHistory()[0];
148 $this->assertEquals(History::CREATED, $actual['event']);
149 $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
150 $this->assertEquals(1, $actual['id']);
151 }
152
153 /**
154 * Re-read history from file after writing an event
155 */
156 public function testHistoryRead()
157 {
158 $history = new History(self::$historyFilePath);
159 $history->updateLink(['id' => 1]);
160 $history = new History(self::$historyFilePath);
161 $actual = $history->getHistory()[0];
162 $this->assertEquals(History::UPDATED, $actual['event']);
163 $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
164 $this->assertEquals(1, $actual['id']);
165 }
166
167 /**
168 * Re-read history from file after writing an event and make sure that the order is correct
169 */
170 public function testHistoryOrderRead()
171 {
172 $history = new History(self::$historyFilePath);
173 $history->updateLink(['id' => 1]);
174 $history->addLink(['id' => 1]);
175
176 $history = new History(self::$historyFilePath);
177 $actual = $history->getHistory()[0];
178 $this->assertEquals(History::CREATED, $actual['event']);
179 $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
180 $this->assertEquals(1, $actual['id']);
181
182 $actual = $history->getHistory()[1];
183 $this->assertEquals(History::UPDATED, $actual['event']);
184 $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
185 $this->assertEquals(1, $actual['id']);
186 }
187
188 /**
189 * Test retention time: delete old entries.
190 */
191 public function testHistoryRententionTime()
192 {
193 $history = new History(self::$historyFilePath, 5);
194 $history->updateLink(['id' => 1]);
195 $this->assertEquals(1, count($history->getHistory()));
196 $arr = $history->getHistory();
197 $arr[0]['datetime'] = (new DateTime('-1 hour'))->format(DateTime::ATOM);
198 FileUtils::writeFlatDB(self::$historyFilePath, $arr);
199
200 $history = new History(self::$historyFilePath, 60);
201 $this->assertEquals(1, count($history->getHistory()));
202 $this->assertEquals(1, $history->getHistory()[0]['id']);
203 $history->updateLink(['id' => 2]);
204 $this->assertEquals(1, count($history->getHistory()));
205 $this->assertEquals(2, $history->getHistory()[0]['id']);
206 }
207}
diff --git a/tests/LinkDBTest.php b/tests/LinkDBTest.php
index 1f62a34a..7bf98f92 100644
--- a/tests/LinkDBTest.php
+++ b/tests/LinkDBTest.php
@@ -101,7 +101,7 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
101 * Attempt to instantiate a LinkDB whereas the datastore is not writable 101 * Attempt to instantiate a LinkDB whereas the datastore is not writable
102 * 102 *
103 * @expectedException IOException 103 * @expectedException IOException
104 * @expectedExceptionMessageRegExp /Error accessing\nnull/ 104 * @expectedExceptionMessageRegExp /Error accessing "null"/
105 */ 105 */
106 public function testConstructDatastoreNotWriteable() 106 public function testConstructDatastoreNotWriteable()
107 { 107 {
diff --git a/tests/NetscapeBookmarkUtils/BookmarkImportTest.php b/tests/NetscapeBookmarkUtils/BookmarkImportTest.php
index 5925a8e1..f838f259 100644
--- a/tests/NetscapeBookmarkUtils/BookmarkImportTest.php
+++ b/tests/NetscapeBookmarkUtils/BookmarkImportTest.php
@@ -34,6 +34,11 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
34 protected static $testDatastore = 'sandbox/datastore.php'; 34 protected static $testDatastore = 'sandbox/datastore.php';
35 35
36 /** 36 /**
37 * @var string History file path
38 */
39 protected static $historyFilePath = 'sandbox/history.php';
40
41 /**
37 * @var LinkDB private LinkDB instance 42 * @var LinkDB private LinkDB instance
38 */ 43 */
39 protected $linkDb = null; 44 protected $linkDb = null;
@@ -49,6 +54,11 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
49 protected $conf; 54 protected $conf;
50 55
51 /** 56 /**
57 * @var History instance.
58 */
59 protected $history;
60
61 /**
52 * @var string Save the current timezone. 62 * @var string Save the current timezone.
53 */ 63 */
54 protected static $defaultTimeZone; 64 protected static $defaultTimeZone;
@@ -73,6 +83,15 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
73 $this->linkDb = new LinkDB(self::$testDatastore, true, false); 83 $this->linkDb = new LinkDB(self::$testDatastore, true, false);
74 $this->conf = new ConfigManager('tests/utils/config/configJson'); 84 $this->conf = new ConfigManager('tests/utils/config/configJson');
75 $this->conf->set('resource.page_cache', $this->pagecache); 85 $this->conf->set('resource.page_cache', $this->pagecache);
86 $this->history = new History(self::$historyFilePath);
87 }
88
89 /**
90 * Delete history file.
91 */
92 public function tearDown()
93 {
94 @unlink(self::$historyFilePath);
76 } 95 }
77 96
78 public static function tearDownAfterClass() 97 public static function tearDownAfterClass()
@@ -89,7 +108,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
89 $this->assertEquals( 108 $this->assertEquals(
90 'File empty.htm (0 bytes) has an unknown file format.' 109 'File empty.htm (0 bytes) has an unknown file format.'
91 .' Nothing was imported.', 110 .' Nothing was imported.',
92 NetscapeBookmarkUtils::import(NULL, $files, NULL, $this->conf) 111 NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history)
93 ); 112 );
94 $this->assertEquals(0, count($this->linkDb)); 113 $this->assertEquals(0, count($this->linkDb));
95 } 114 }
@@ -102,7 +121,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
102 $files = file2array('no_doctype.htm'); 121 $files = file2array('no_doctype.htm');
103 $this->assertEquals( 122 $this->assertEquals(
104 'File no_doctype.htm (350 bytes) has an unknown file format. Nothing was imported.', 123 'File no_doctype.htm (350 bytes) has an unknown file format. Nothing was imported.',
105 NetscapeBookmarkUtils::import(NULL, $files, NULL, $this->conf) 124 NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history)
106 ); 125 );
107 $this->assertEquals(0, count($this->linkDb)); 126 $this->assertEquals(0, count($this->linkDb));
108 } 127 }
@@ -116,7 +135,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
116 $this->assertEquals( 135 $this->assertEquals(
117 'File internet_explorer_encoding.htm (356 bytes) was successfully processed:' 136 'File internet_explorer_encoding.htm (356 bytes) was successfully processed:'
118 .' 1 links imported, 0 links overwritten, 0 links skipped.', 137 .' 1 links imported, 0 links overwritten, 0 links skipped.',
119 NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf) 138 NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
120 ); 139 );
121 $this->assertEquals(1, count($this->linkDb)); 140 $this->assertEquals(1, count($this->linkDb));
122 $this->assertEquals(0, count_private($this->linkDb)); 141 $this->assertEquals(0, count_private($this->linkDb));
@@ -145,7 +164,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
145 $this->assertEquals( 164 $this->assertEquals(
146 'File netscape_nested.htm (1337 bytes) was successfully processed:' 165 'File netscape_nested.htm (1337 bytes) was successfully processed:'
147 .' 8 links imported, 0 links overwritten, 0 links skipped.', 166 .' 8 links imported, 0 links overwritten, 0 links skipped.',
148 NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf) 167 NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
149 ); 168 );
150 $this->assertEquals(8, count($this->linkDb)); 169 $this->assertEquals(8, count($this->linkDb));
151 $this->assertEquals(2, count_private($this->linkDb)); 170 $this->assertEquals(2, count_private($this->linkDb));
@@ -267,7 +286,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
267 $this->assertEquals( 286 $this->assertEquals(
268 'File netscape_basic.htm (482 bytes) was successfully processed:' 287 'File netscape_basic.htm (482 bytes) was successfully processed:'
269 .' 2 links imported, 0 links overwritten, 0 links skipped.', 288 .' 2 links imported, 0 links overwritten, 0 links skipped.',
270 NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf) 289 NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
271 ); 290 );
272 291
273 $this->assertEquals(2, count($this->linkDb)); 292 $this->assertEquals(2, count($this->linkDb));
@@ -312,7 +331,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
312 $this->assertEquals( 331 $this->assertEquals(
313 'File netscape_basic.htm (482 bytes) was successfully processed:' 332 'File netscape_basic.htm (482 bytes) was successfully processed:'
314 .' 2 links imported, 0 links overwritten, 0 links skipped.', 333 .' 2 links imported, 0 links overwritten, 0 links skipped.',
315 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 334 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
316 ); 335 );
317 $this->assertEquals(2, count($this->linkDb)); 336 $this->assertEquals(2, count($this->linkDb));
318 $this->assertEquals(1, count_private($this->linkDb)); 337 $this->assertEquals(1, count_private($this->linkDb));
@@ -356,7 +375,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
356 $this->assertEquals( 375 $this->assertEquals(
357 'File netscape_basic.htm (482 bytes) was successfully processed:' 376 'File netscape_basic.htm (482 bytes) was successfully processed:'
358 .' 2 links imported, 0 links overwritten, 0 links skipped.', 377 .' 2 links imported, 0 links overwritten, 0 links skipped.',
359 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 378 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
360 ); 379 );
361 $this->assertEquals(2, count($this->linkDb)); 380 $this->assertEquals(2, count($this->linkDb));
362 $this->assertEquals(0, count_private($this->linkDb)); 381 $this->assertEquals(0, count_private($this->linkDb));
@@ -380,7 +399,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
380 $this->assertEquals( 399 $this->assertEquals(
381 'File netscape_basic.htm (482 bytes) was successfully processed:' 400 'File netscape_basic.htm (482 bytes) was successfully processed:'
382 .' 2 links imported, 0 links overwritten, 0 links skipped.', 401 .' 2 links imported, 0 links overwritten, 0 links skipped.',
383 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 402 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
384 ); 403 );
385 $this->assertEquals(2, count($this->linkDb)); 404 $this->assertEquals(2, count($this->linkDb));
386 $this->assertEquals(2, count_private($this->linkDb)); 405 $this->assertEquals(2, count_private($this->linkDb));
@@ -406,7 +425,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
406 $this->assertEquals( 425 $this->assertEquals(
407 'File netscape_basic.htm (482 bytes) was successfully processed:' 426 'File netscape_basic.htm (482 bytes) was successfully processed:'
408 .' 2 links imported, 0 links overwritten, 0 links skipped.', 427 .' 2 links imported, 0 links overwritten, 0 links skipped.',
409 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 428 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
410 ); 429 );
411 $this->assertEquals(2, count($this->linkDb)); 430 $this->assertEquals(2, count($this->linkDb));
412 $this->assertEquals(2, count_private($this->linkDb)); 431 $this->assertEquals(2, count_private($this->linkDb));
@@ -426,7 +445,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
426 $this->assertEquals( 445 $this->assertEquals(
427 'File netscape_basic.htm (482 bytes) was successfully processed:' 446 'File netscape_basic.htm (482 bytes) was successfully processed:'
428 .' 2 links imported, 2 links overwritten, 0 links skipped.', 447 .' 2 links imported, 2 links overwritten, 0 links skipped.',
429 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 448 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
430 ); 449 );
431 $this->assertEquals(2, count($this->linkDb)); 450 $this->assertEquals(2, count($this->linkDb));
432 $this->assertEquals(0, count_private($this->linkDb)); 451 $this->assertEquals(0, count_private($this->linkDb));
@@ -452,7 +471,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
452 $this->assertEquals( 471 $this->assertEquals(
453 'File netscape_basic.htm (482 bytes) was successfully processed:' 472 'File netscape_basic.htm (482 bytes) was successfully processed:'
454 .' 2 links imported, 0 links overwritten, 0 links skipped.', 473 .' 2 links imported, 0 links overwritten, 0 links skipped.',
455 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 474 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
456 ); 475 );
457 $this->assertEquals(2, count($this->linkDb)); 476 $this->assertEquals(2, count($this->linkDb));
458 $this->assertEquals(0, count_private($this->linkDb)); 477 $this->assertEquals(0, count_private($this->linkDb));
@@ -473,7 +492,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
473 $this->assertEquals( 492 $this->assertEquals(
474 'File netscape_basic.htm (482 bytes) was successfully processed:' 493 'File netscape_basic.htm (482 bytes) was successfully processed:'
475 .' 2 links imported, 2 links overwritten, 0 links skipped.', 494 .' 2 links imported, 2 links overwritten, 0 links skipped.',
476 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 495 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
477 ); 496 );
478 $this->assertEquals(2, count($this->linkDb)); 497 $this->assertEquals(2, count($this->linkDb));
479 $this->assertEquals(2, count_private($this->linkDb)); 498 $this->assertEquals(2, count_private($this->linkDb));
@@ -497,7 +516,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
497 $this->assertEquals( 516 $this->assertEquals(
498 'File netscape_basic.htm (482 bytes) was successfully processed:' 517 'File netscape_basic.htm (482 bytes) was successfully processed:'
499 .' 2 links imported, 0 links overwritten, 0 links skipped.', 518 .' 2 links imported, 0 links overwritten, 0 links skipped.',
500 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 519 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
501 ); 520 );
502 $this->assertEquals(2, count($this->linkDb)); 521 $this->assertEquals(2, count($this->linkDb));
503 $this->assertEquals(0, count_private($this->linkDb)); 522 $this->assertEquals(0, count_private($this->linkDb));
@@ -507,7 +526,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
507 $this->assertEquals( 526 $this->assertEquals(
508 'File netscape_basic.htm (482 bytes) was successfully processed:' 527 'File netscape_basic.htm (482 bytes) was successfully processed:'
509 .' 0 links imported, 0 links overwritten, 2 links skipped.', 528 .' 0 links imported, 0 links overwritten, 2 links skipped.',
510 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 529 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
511 ); 530 );
512 $this->assertEquals(2, count($this->linkDb)); 531 $this->assertEquals(2, count($this->linkDb));
513 $this->assertEquals(0, count_private($this->linkDb)); 532 $this->assertEquals(0, count_private($this->linkDb));
@@ -526,7 +545,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
526 $this->assertEquals( 545 $this->assertEquals(
527 'File netscape_basic.htm (482 bytes) was successfully processed:' 546 'File netscape_basic.htm (482 bytes) was successfully processed:'
528 .' 2 links imported, 0 links overwritten, 0 links skipped.', 547 .' 2 links imported, 0 links overwritten, 0 links skipped.',
529 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 548 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
530 ); 549 );
531 $this->assertEquals(2, count($this->linkDb)); 550 $this->assertEquals(2, count($this->linkDb));
532 $this->assertEquals(0, count_private($this->linkDb)); 551 $this->assertEquals(0, count_private($this->linkDb));
@@ -553,7 +572,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
553 $this->assertEquals( 572 $this->assertEquals(
554 'File netscape_basic.htm (482 bytes) was successfully processed:' 573 'File netscape_basic.htm (482 bytes) was successfully processed:'
555 .' 2 links imported, 0 links overwritten, 0 links skipped.', 574 .' 2 links imported, 0 links overwritten, 0 links skipped.',
556 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf) 575 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
557 ); 576 );
558 $this->assertEquals(2, count($this->linkDb)); 577 $this->assertEquals(2, count($this->linkDb));
559 $this->assertEquals(0, count_private($this->linkDb)); 578 $this->assertEquals(0, count_private($this->linkDb));
@@ -578,7 +597,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
578 $this->assertEquals( 597 $this->assertEquals(
579 'File same_date.htm (453 bytes) was successfully processed:' 598 'File same_date.htm (453 bytes) was successfully processed:'
580 .' 3 links imported, 0 links overwritten, 0 links skipped.', 599 .' 3 links imported, 0 links overwritten, 0 links skipped.',
581 NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf) 600 NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf, $this->history)
582 ); 601 );
583 $this->assertEquals(3, count($this->linkDb)); 602 $this->assertEquals(3, count($this->linkDb));
584 $this->assertEquals(0, count_private($this->linkDb)); 603 $this->assertEquals(0, count_private($this->linkDb));
@@ -595,4 +614,32 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
595 $this->linkDb[2]['id'] 614 $this->linkDb[2]['id']
596 ); 615 );
597 } 616 }
617
618 public function testImportCreateUpdateHistory()
619 {
620 $post = [
621 'privacy' => 'public',
622 'overwrite' => 'true',
623 ];
624 $files = file2array('netscape_basic.htm');
625 $nbLinks = 2;
626 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history);
627 $history = $this->history->getHistory();
628 $this->assertEquals($nbLinks, count($history));
629 foreach ($history as $value) {
630 $this->assertEquals(History::CREATED, $value['event']);
631 $this->assertTrue(new DateTime('-5 seconds') < DateTime::createFromFormat(DateTime::ATOM, $value['datetime']));
632 $this->assertTrue(is_int($value['id']));
633 }
634
635 // re-import as private, enable overwriting
636 NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history);
637 $history = $this->history->getHistory();
638 $this->assertEquals($nbLinks * 2, count($history));
639 for ($i = 0 ; $i < $nbLinks ; $i++) {
640 $this->assertEquals(History::UPDATED, $history[$i]['event']);
641 $this->assertTrue(new DateTime('-5 seconds') < DateTime::createFromFormat(DateTime::ATOM, $history[$i]['datetime']));
642 $this->assertTrue(is_int($history[$i]['id']));
643 }
644 }
598} 645}
diff --git a/tests/TimeZoneTest.php b/tests/TimeZoneTest.php
index 2976d116..127fdc19 100644
--- a/tests/TimeZoneTest.php
+++ b/tests/TimeZoneTest.php
@@ -11,24 +11,45 @@ require_once 'application/TimeZone.php';
11class TimeZoneTest extends PHPUnit_Framework_TestCase 11class TimeZoneTest extends PHPUnit_Framework_TestCase
12{ 12{
13 /** 13 /**
14 * @var array of timezones
15 */
16 protected $installedTimezones;
17
18 public function setUp()
19 {
20 $this->installedTimezones = [
21 'Antarctica/Syowa',
22 'Europe/London',
23 'Europe/Paris',
24 'UTC'
25 ];
26 }
27
28 /**
14 * Generate a timezone selection form 29 * Generate a timezone selection form
15 */ 30 */
16 public function testGenerateTimeZoneForm() 31 public function testGenerateTimeZoneForm()
17 { 32 {
18 $generated = generateTimeZoneForm(); 33 $expected = [
34 'continents' => [
35 'Antarctica',
36 'Europe',
37 'UTC',
38 'selected' => '',
39 ],
40 'cities' => [
41 ['continent' => 'Antarctica', 'city' => 'Syowa'],
42 ['continent' => 'Europe', 'city' => 'London'],
43 ['continent' => 'Europe', 'city' => 'Paris'],
44 ['continent' => 'UTC', 'city' => 'UTC'],
45 'selected' => '',
46 ]
47 ];
19 48
20 // HTML form 49 list($continents, $cities) = generateTimeZoneData($this->installedTimezones);
21 $this->assertStringStartsWith('Continent:<select', $generated[0]);
22 $this->assertContains('selected="selected"', $generated[0]);
23 $this->assertStringEndsWith('</select><br />', $generated[0]);
24 50
25 // Javascript handler 51 $this->assertEquals($expected['continents'], $continents);
26 $this->assertStringStartsWith('<script>', $generated[1]); 52 $this->assertEquals($expected['cities'], $cities);
27 $this->assertContains(
28 '<option value=\"Bermuda\">Bermuda<\/option>',
29 $generated[1]
30 );
31 $this->assertStringEndsWith('</script>', $generated[1]);
32 } 53 }
33 54
34 /** 55 /**
@@ -36,28 +57,26 @@ class TimeZoneTest extends PHPUnit_Framework_TestCase
36 */ 57 */
37 public function testGenerateTimeZoneFormPreselected() 58 public function testGenerateTimeZoneFormPreselected()
38 { 59 {
39 $generated = generateTimeZoneForm('Antarctica/Syowa'); 60 $expected = [
40 61 'continents' => [
41 // HTML form 62 'Antarctica',
42 $this->assertStringStartsWith('Continent:<select', $generated[0]); 63 'Europe',
43 $this->assertContains( 64 'UTC',
44 'value="Antarctica" selected="selected"', 65 'selected' => 'Antarctica',
45 $generated[0] 66 ],
46 ); 67 'cities' => [
47 $this->assertContains( 68 ['continent' => 'Antarctica', 'city' => 'Syowa'],
48 'value="Syowa" selected="selected"', 69 ['continent' => 'Europe', 'city' => 'London'],
49 $generated[0] 70 ['continent' => 'Europe', 'city' => 'Paris'],
50 ); 71 ['continent' => 'UTC', 'city' => 'UTC'],
51 $this->assertStringEndsWith('</select><br />', $generated[0]); 72 'selected' => 'Syowa',
73 ]
74 ];
52 75
76 list($continents, $cities) = generateTimeZoneData($this->installedTimezones, 'Antarctica/Syowa');
53 77
54 // Javascript handler 78 $this->assertEquals($expected['continents'], $continents);
55 $this->assertStringStartsWith('<script>', $generated[1]); 79 $this->assertEquals($expected['cities'], $cities);
56 $this->assertContains(
57 '<option value=\"Bermuda\">Bermuda<\/option>',
58 $generated[1]
59 );
60 $this->assertStringEndsWith('</script>', $generated[1]);
61 } 80 }
62 81
63 /** 82 /**
diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php
index e70cc1ae..d6a0aad5 100644
--- a/tests/UtilsTest.php
+++ b/tests/UtilsTest.php
@@ -4,6 +4,7 @@
4 */ 4 */
5 5
6require_once 'application/Utils.php'; 6require_once 'application/Utils.php';
7require_once 'application/Languages.php';
7require_once 'tests/utils/ReferenceSessionIdHashes.php'; 8require_once 'tests/utils/ReferenceSessionIdHashes.php';
8 9
9// Initialize reference data before PHPUnit starts a session 10// Initialize reference data before PHPUnit starts a session
@@ -326,4 +327,94 @@ class UtilsTest extends PHPUnit_Framework_TestCase
326 $this->assertFalse(format_date([])); 327 $this->assertFalse(format_date([]));
327 $this->assertFalse(format_date(null)); 328 $this->assertFalse(format_date(null));
328 } 329 }
330
331 /**
332 * Test is_integer_mixed with valid values
333 */
334 public function testIsIntegerMixedValid()
335 {
336 $this->assertTrue(is_integer_mixed(12));
337 $this->assertTrue(is_integer_mixed('12'));
338 $this->assertTrue(is_integer_mixed(-12));
339 $this->assertTrue(is_integer_mixed('-12'));
340 $this->assertTrue(is_integer_mixed(0));
341 $this->assertTrue(is_integer_mixed('0'));
342 $this->assertTrue(is_integer_mixed(0x0a));
343 }
344
345 /**
346 * Test is_integer_mixed with invalid values
347 */
348 public function testIsIntegerMixedInvalid()
349 {
350 $this->assertFalse(is_integer_mixed(true));
351 $this->assertFalse(is_integer_mixed(false));
352 $this->assertFalse(is_integer_mixed([]));
353 $this->assertFalse(is_integer_mixed(['test']));
354 $this->assertFalse(is_integer_mixed([12]));
355 $this->assertFalse(is_integer_mixed(new DateTime()));
356 $this->assertFalse(is_integer_mixed('0x0a'));
357 $this->assertFalse(is_integer_mixed('12k'));
358 $this->assertFalse(is_integer_mixed('k12'));
359 $this->assertFalse(is_integer_mixed(''));
360 }
361
362 /**
363 * Test return_bytes
364 */
365 public function testReturnBytes()
366 {
367 $this->assertEquals(2 * 1024, return_bytes('2k'));
368 $this->assertEquals(2 * 1024, return_bytes('2K'));
369 $this->assertEquals(2 * (pow(1024, 2)), return_bytes('2m'));
370 $this->assertEquals(2 * (pow(1024, 2)), return_bytes('2M'));
371 $this->assertEquals(2 * (pow(1024, 3)), return_bytes('2g'));
372 $this->assertEquals(2 * (pow(1024, 3)), return_bytes('2G'));
373 $this->assertEquals(374, return_bytes('374'));
374 $this->assertEquals(374, return_bytes(374));
375 $this->assertEquals(0, return_bytes('0'));
376 $this->assertEquals(0, return_bytes(0));
377 $this->assertEquals(-1, return_bytes('-1'));
378 $this->assertEquals(-1, return_bytes(-1));
379 $this->assertEquals('', return_bytes(''));
380 }
381
382 /**
383 * Test human_bytes
384 */
385 public function testHumanBytes()
386 {
387 $this->assertEquals('2kiB', human_bytes(2 * 1024));
388 $this->assertEquals('2kiB', human_bytes(strval(2 * 1024)));
389 $this->assertEquals('2MiB', human_bytes(2 * (pow(1024, 2))));
390 $this->assertEquals('2MiB', human_bytes(strval(2 * (pow(1024, 2)))));
391 $this->assertEquals('2GiB', human_bytes(2 * (pow(1024, 3))));
392 $this->assertEquals('2GiB', human_bytes(strval(2 * (pow(1024, 3)))));
393 $this->assertEquals('374B', human_bytes(374));
394 $this->assertEquals('374B', human_bytes('374'));
395 $this->assertEquals('232kiB', human_bytes(237481));
396 $this->assertEquals('Unlimited', human_bytes('0'));
397 $this->assertEquals('Unlimited', human_bytes(0));
398 $this->assertEquals('Setting not set', human_bytes(''));
399 }
400
401 /**
402 * Test get_max_upload_size with formatting
403 */
404 public function testGetMaxUploadSize()
405 {
406 $this->assertEquals('1MiB', get_max_upload_size(2097152, '1024k'));
407 $this->assertEquals('1MiB', get_max_upload_size('1m', '2m'));
408 $this->assertEquals('100B', get_max_upload_size(100, 100));
409 }
410
411 /**
412 * Test get_max_upload_size without formatting
413 */
414 public function testGetMaxUploadSizeRaw()
415 {
416 $this->assertEquals('1048576', get_max_upload_size(2097152, '1024k', false));
417 $this->assertEquals('1048576', get_max_upload_size('1m', '2m', false));
418 $this->assertEquals('100', get_max_upload_size(100, 100, false));
419 }
329} 420}
diff --git a/tests/plugins/PluginReadityourselfTest.php b/tests/plugins/PluginReadityourselfTest.php
deleted file mode 100644
index bbba9676..00000000
--- a/tests/plugins/PluginReadityourselfTest.php
+++ /dev/null
@@ -1,99 +0,0 @@
1<?php
2use Shaarli\Config\ConfigManager;
3
4/**
5 * PluginReadityourselfTest.php.php
6 */
7
8require_once 'plugins/readityourself/readityourself.php';
9
10/**
11 * Class PluginWallabagTest
12 * Unit test for the Wallabag plugin
13 */
14class PluginReadityourselfTest extends PHPUnit_Framework_TestCase
15{
16 /**
17 * Reset plugin path
18 */
19 public function setUp()
20 {
21 PluginManager::$PLUGINS_PATH = 'plugins';
22 }
23
24 /**
25 * Test Readityourself init without errors.
26 */
27 public function testReadityourselfInitNoError()
28 {
29 $conf = new ConfigManager('');
30 $conf->set('plugins.READITYOUSELF_URL', 'value');
31 $errors = readityourself_init($conf);
32 $this->assertEmpty($errors);
33 }
34
35 /**
36 * Test Readityourself init with errors.
37 */
38 public function testReadityourselfInitError()
39 {
40 $conf = new ConfigManager('');
41 $errors = readityourself_init($conf);
42 $this->assertNotEmpty($errors);
43 }
44
45 /**
46 * Test render_linklist hook.
47 */
48 public function testReadityourselfLinklist()
49 {
50 $conf = new ConfigManager('');
51 $conf->set('plugins.READITYOUSELF_URL', 'value');
52 $str = 'http://randomstr.com/test';
53 $data = array(
54 'title' => $str,
55 'links' => array(
56 array(
57 'url' => $str,
58 )
59 )
60 );
61
62 $data = hook_readityourself_render_linklist($data, $conf);
63 $link = $data['links'][0];
64 // data shouldn't be altered
65 $this->assertEquals($str, $data['title']);
66 $this->assertEquals($str, $link['url']);
67
68 // plugin data
69 $this->assertEquals(1, count($link['link_plugin']));
70 $this->assertNotFalse(strpos($link['link_plugin'][0], $str));
71 }
72
73 /**
74 * Test without config: nothing should happened.
75 */
76 public function testReadityourselfLinklistWithoutConfig()
77 {
78 $conf = new ConfigManager('');
79 $conf->set('plugins.READITYOUSELF_URL', null);
80 $str = 'http://randomstr.com/test';
81 $data = array(
82 'title' => $str,
83 'links' => array(
84 array(
85 'url' => $str,
86 )
87 )
88 );
89
90 $data = hook_readityourself_render_linklist($data, $conf);
91 $link = $data['links'][0];
92 // data shouldn't be altered
93 $this->assertEquals($str, $data['title']);
94 $this->assertEquals($str, $link['url']);
95
96 // plugin data
97 $this->assertArrayNotHasKey('link_plugin', $link);
98 }
99}
diff --git a/tpl/default/configure.html b/tpl/default/configure.html
index 12261487..76a1b9fd 100644
--- a/tpl/default/configure.html
+++ b/tpl/default/configure.html
@@ -34,7 +34,7 @@
34 <div class="pure-u-lg-{$ratioLabel} pure-u-1"> 34 <div class="pure-u-lg-{$ratioLabel} pure-u-1">
35 <div class="form-label"> 35 <div class="form-label">
36 <label for="titleLink"> 36 <label for="titleLink">
37 <span class="label-name">{'Title link'|t}</span><br> 37 <span class="label-name">{'Home link'|t}</span><br>
38 <span class="label-desc">{'Default value'|t}: ?</span> 38 <span class="label-desc">{'Default value'|t}: ?</span>
39 </label> 39 </label>
40 </div> 40 </div>
@@ -73,15 +73,35 @@
73 <div class="pure-u-lg-{$ratioLabel} pure-u-1 "> 73 <div class="pure-u-lg-{$ratioLabel} pure-u-1 ">
74 <div class="form-label"> 74 <div class="form-label">
75 <label> 75 <label>
76 <span class="label-name">{'Timezone'|t}</span> 76 <span class="label-name">{'Timezone'|t}</span><br>
77 <span class="label-desc">{'Continent'|t} &middot; {'City'|t}</span>
77 </label> 78 </label>
78 </div> 79 </div>
79 </div> 80 </div>
80 <div class="pure-u-lg-{$ratioInput} pure-u-1 "> 81 <div class="pure-u-lg-{$ratioInput} pure-u-1 ">
81 <div class="form-input"> 82 <div class="form-input">
82 {ignore}FIXME! too hackish, needs to be fixed upstream{/ignore} 83 <div class="timezone">
83 <div class="timezone" id="timezone-remove">{$timezone_form}</div> 84 <select id="continent" name="continent">
84 <div class="timezone" id="timezone-add"></div> 85 {loop="$continents"}
86 {if="$key !== 'selected'"}
87 <option value="{$value}" {if="$continents.selected === $value"}selected{/if}>
88 {$value}
89 </option>
90 {/if}
91 {/loop}
92 </select>
93 <select id="city" name="city">
94 {loop="$cities"}
95 {if="$key !== 'selected'"}
96 <option value="{$value.city}"
97 {if="$cities.selected === $value.city"}selected{/if}
98 data-continent="{$value.continent}">
99 {$value.city}
100 </option>
101 {/if}
102 {/loop}
103 </select>
104 </div>
85 </div> 105 </div>
86 </div> 106 </div>
87 </div> 107 </div>
diff --git a/tpl/default/import.html b/tpl/default/import.html
index e6e521e8..1f040685 100644
--- a/tpl/default/import.html
+++ b/tpl/default/import.html
@@ -18,6 +18,7 @@
18 <div class="center" id="import-field"> 18 <div class="center" id="import-field">
19 <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}"> 19 <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}">
20 <input type="file" name="filetoupload"> 20 <input type="file" name="filetoupload">
21 <p><br>Maximum size allowed: <strong>{$maxfilesizeHuman}</strong></p>
21 </div> 22 </div>
22 23
23 <div class="pure-g"> 24 <div class="pure-g">
diff --git a/tpl/default/install.html b/tpl/default/install.html
index 663397ac..164d453b 100644
--- a/tpl/default/install.html
+++ b/tpl/default/install.html
@@ -45,24 +45,22 @@
45 </div> 45 </div>
46 <div class="pure-u-lg-{$ratioInput} pure-u-1"> 46 <div class="pure-u-lg-{$ratioInput} pure-u-1">
47 <div class="form-input"> 47 <div class="form-input">
48 <input type="text" name="setpassword" id="password"> 48 <input type="password" name="setpassword" id="password">
49 </div> 49 </div>
50 </div> 50 </div>
51 </div> 51 </div>
52 52
53 <div class="pure-g"> 53 <div class="pure-g">
54 <div class="pure-u-lg-{$ratioLabel} pure-u-1 "> 54 <div class="pure-u-lg-{$ratioLabel} pure-u-1">
55 <div class="form-label"> 55 <div class="form-label">
56 <label> 56 <label for="title">
57 <span class="label-name">{'Timezone'|t}</span> 57 <span class="label-name">{'Shaarli title'|t}</span>
58 </label> 58 </label>
59 </div> 59 </div>
60 </div> 60 </div>
61 <div class="pure-u-lg-{$ratioInput} pure-u-1 "> 61 <div class="pure-u-lg-{$ratioInput} pure-u-1">
62 <div class="form-input"> 62 <div class="form-input">
63 {ignore}FIXME! too hackish, needs to be fixed upstream{/ignore} 63 <input type="text" name="title" id="title" placeholder="{'My links'|t}">
64 <div class="timezone" id="timezone-remove">{$timezone_html}</div>
65 <div class="timezone" id="timezone-add"></div>
66 </div> 64 </div>
67 </div> 65 </div>
68 </div> 66 </div>
@@ -70,14 +68,36 @@
70 <div class="pure-g"> 68 <div class="pure-g">
71 <div class="pure-u-lg-{$ratioLabel} pure-u-1"> 69 <div class="pure-u-lg-{$ratioLabel} pure-u-1">
72 <div class="form-label"> 70 <div class="form-label">
73 <label for="title"> 71 <label>
74 <span class="label-name">{'Shaarli title'|t}</span> 72 <span class="label-name">{'Timezone'|t}</span><br>
73 <span class="label-desc">{'Continent'|t} &middot; {'City'|t}</span>
75 </label> 74 </label>
76 </div> 75 </div>
77 </div> 76 </div>
78 <div class="pure-u-lg-{$ratioInput} pure-u-1"> 77 <div class="pure-u-lg-{$ratioInput} pure-u-1">
79 <div class="form-input"> 78 <div class="form-input">
80 <input type="text" name="title" id="title" placeholder="{'My links'|t}"> 79 <div class="timezone">
80 <select id="continent" name="continent">
81 {loop="$continents"}
82 {if="$key !== 'selected'"}
83 <option value="{$value}" {if="$continents.selected === $value"}selected{/if}>
84 {$value}
85 </option>
86 {/if}
87 {/loop}
88 </select>
89 <select id="city" name="city">
90 {loop="$cities"}
91 {if="$key !== 'selected'"}
92 <option value="{$value.city}"
93 {if="$cities.selected === $value.city"}selected{/if}
94 data-continent="{$value.continent}">
95 {$value.city}
96 </option>
97 {/if}
98 {/loop}
99 </select>
100 </div>
81 </div> 101 </div>
82 </div> 102 </div>
83 </div> 103 </div>
diff --git a/tpl/default/js/shaarli.js b/tpl/default/js/shaarli.js
index edcf2809..4d47fcd0 100644
--- a/tpl/default/js/shaarli.js
+++ b/tpl/default/js/shaarli.js
@@ -76,9 +76,12 @@ window.onload = function () {
76 } 76 }
77 } 77 }
78 78
79 document.getElementById('menu-toggle').addEventListener('click', function (e) { 79 var menuToggle = document.getElementById('menu-toggle');
80 toggleMenu(); 80 if (menuToggle != null) {
81 }); 81 menuToggle.addEventListener('click', function (e) {
82 toggleMenu();
83 });
84 }
82 85
83 window.addEventListener(WINDOW_CHANGE_EVENT, closeMenu); 86 window.addEventListener(WINDOW_CHANGE_EVENT, closeMenu);
84 })(this, this.document); 87 })(this, this.document);
@@ -299,21 +302,6 @@ window.onload = function () {
299 } 302 }
300 303
301 /** 304 /**
302 * TimeZome select
303 * FIXME! way too hackish
304 */
305 var toRemove = document.getElementById('timezone-remove');
306 if (toRemove != null) {
307 var firstSelect = toRemove.getElementsByTagName('select')[0];
308 var secondSelect = toRemove.getElementsByTagName('select')[1];
309 toRemove.parentNode.removeChild(toRemove);
310 var toAdd = document.getElementById('timezone-add');
311 var newTimezone = '<span class="timezone-continent">Continent ' + firstSelect.outerHTML + '</span>';
312 newTimezone += ' <span class="timezone-country">Country ' + secondSelect.outerHTML + '</span>';
313 toAdd.innerHTML = newTimezone;
314 }
315
316 /**
317 * Awesomplete trigger. 305 * Awesomplete trigger.
318 */ 306 */
319 var tags = document.getElementById('lf_tags'); 307 var tags = document.getElementById('lf_tags');
@@ -365,6 +353,15 @@ window.onload = function () {
365 } 353 }
366 }); 354 });
367 }); 355 });
356
357 var continent = document.getElementById('continent');
358 var city = document.getElementById('city');
359 if (continent != null && city != null) {
360 continent.addEventListener('change', function(event) {
361 hideTimezoneCities(city, continent.options[continent.selectedIndex].value, true);
362 });
363 hideTimezoneCities(city, continent.options[continent.selectedIndex].value, false);
364 }
368}; 365};
369 366
370function activateFirefoxSocial(node) { 367function activateFirefoxSocial(node) {
@@ -390,3 +387,25 @@ function activateFirefoxSocial(node) {
390 var activate = new CustomEvent("ActivateSocialFeature"); 387 var activate = new CustomEvent("ActivateSocialFeature");
391 node.dispatchEvent(activate); 388 node.dispatchEvent(activate);
392} 389}
390
391/**
392 * Add the class 'hidden' to city options not attached to the current selected continent.
393 *
394 * @param cities List of <option> elements
395 * @param currentContinent Current selected continent
396 * @param reset Set to true to reset the selected value
397 */
398function hideTimezoneCities(cities, currentContinent, reset = false) {
399 var first = true;
400 [].forEach.call(cities, function(option) {
401 if (option.getAttribute('data-continent') != currentContinent) {
402 option.className = 'hidden';
403 } else {
404 option.className = '';
405 if (reset === true && first === true) {
406 option.setAttribute('selected', 'selected');
407 first = false;
408 }
409 }
410 });
411}
diff --git a/tpl/vintage/configure.html b/tpl/vintage/configure.html
index 704389c5..479284eb 100644
--- a/tpl/vintage/configure.html
+++ b/tpl/vintage/configure.html
@@ -4,7 +4,6 @@
4<body onload="document.configform.title.focus();"> 4<body onload="document.configform.title.focus();">
5<div id="pageheader"> 5<div id="pageheader">
6 {include="page.header"} 6 {include="page.header"}
7 {$timezone_js}
8 <form method="POST" action="#" name="configform" id="configform"> 7 <form method="POST" action="#" name="configform" id="configform">
9 <input type="hidden" name="token" value="{$token}"> 8 <input type="hidden" name="token" value="{$token}">
10 <table id="configuration_table"> 9 <table id="configuration_table">
@@ -15,7 +14,7 @@
15 </tr> 14 </tr>
16 15
17 <tr> 16 <tr>
18 <td><b>Title link:</b></td> 17 <td><b>Home link:</b></td>
19 <td><input type="text" name="titleLink" id="titleLink" size="50" value="{$titleLink}"><br/><label 18 <td><input type="text" name="titleLink" id="titleLink" size="50" value="{$titleLink}"><br/><label
20 for="titleLink">(default value is: ?)</label></td> 19 for="titleLink">(default value is: ?)</label></td>
21 </tr> 20 </tr>
@@ -35,7 +34,28 @@
35 34
36 <tr> 35 <tr>
37 <td><b>Timezone:</b></td> 36 <td><b>Timezone:</b></td>
38 <td>{$timezone_form}</td> 37 <td>
38 <select id="continent" name="continent">
39 {loop="$continents"}
40 {if="$key !== 'selected'"}
41 <option value="{$value}" {if="$continents.selected === $value"}selected{/if}>
42 {$value}
43 </option>
44 {/if}
45 {/loop}
46 </select>
47 <select id="city" name="city">
48 {loop="$cities"}
49 {if="$key !== 'selected'"}
50 <option value="{$value.city}"
51 {if="$cities.selected === $value.city"}selected{/if}
52 data-continent="{$value.continent}">
53 {$value.city}
54 </option>
55 {/if}
56 {/loop}
57 </select>
58 </td>
39 </tr> 59 </tr>
40 60
41 <tr> 61 <tr>
diff --git a/tpl/vintage/css/shaarli.css b/tpl/vintage/css/shaarli.css
index 7ca567e7..9c72d993 100644
--- a/tpl/vintage/css/shaarli.css
+++ b/tpl/vintage/css/shaarli.css
@@ -41,6 +41,10 @@ strong {
41 font-weight: bold; 41 font-weight: bold;
42} 42}
43 43
44.hidden {
45 display: none;
46}
47
44/* Buttons */ 48/* Buttons */
45.bigbutton, #pageheader a.bigbutton { 49.bigbutton, #pageheader a.bigbutton {
46 background-color: #c0c0c0; 50 background-color: #c0c0c0;
diff --git a/tpl/vintage/import.html b/tpl/vintage/import.html
index 071e1160..bb9e4a56 100644
--- a/tpl/vintage/import.html
+++ b/tpl/vintage/import.html
@@ -5,7 +5,7 @@
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}).
9 <form method="POST" action="?do=import" enctype="multipart/form-data" 9 <form method="POST" action="?do=import" enctype="multipart/form-data"
10 name="uploadform" id="uploadform"> 10 name="uploadform" id="uploadform">
11 <input type="hidden" name="token" value="{$token}"> 11 <input type="hidden" name="token" value="{$token}">
diff --git a/tpl/vintage/install.html b/tpl/vintage/install.html
index 42874dcd..aca890d6 100644
--- a/tpl/vintage/install.html
+++ b/tpl/vintage/install.html
@@ -1,6 +1,6 @@
1<!DOCTYPE html> 1<!DOCTYPE html>
2<html> 2<html>
3<head>{include="includes"}{$timezone_js}</head> 3<head>{include="includes"}</head>
4<body onload="document.installform.setlogin.focus();"> 4<body onload="document.installform.setlogin.focus();">
5<div id="install"> 5<div id="install">
6 <h1>Shaarli</h1> 6 <h1>Shaarli</h1>
@@ -9,7 +9,31 @@
9 <table> 9 <table>
10 <tr><td><b>Login:</b></td><td><input type="text" name="setlogin" size="30"></td></tr> 10 <tr><td><b>Login:</b></td><td><input type="text" name="setlogin" size="30"></td></tr>
11 <tr><td><b>Password:</b></td><td><input type="password" name="setpassword" size="30"></td></tr> 11 <tr><td><b>Password:</b></td><td><input type="password" name="setpassword" size="30"></td></tr>
12 {$timezone_html} 12 <tr>
13 <td><b>Timezone:</b></td>
14 <td>
15 <select id="continent" name="continent">
16 {loop="$continents"}
17 {if="$key !== 'selected'"}
18 <option value="{$value}" {if="$continents.selected === $value"}selected{/if}>
19 {$value}
20 </option>
21 {/if}
22 {/loop}
23 </select>
24 <select id="city" name="city">
25 {loop="$cities"}
26 {if="$key !== 'selected'"}
27 <option value="{$value.city}"
28 {if="$cities.selected === $value.city"}selected{/if}
29 data-continent="{$value.continent}">
30 {$value.city}
31 </option>
32 {/if}
33 {/loop}
34 </select>
35 </td>
36 </tr>
13 <tr><td><b>Page title:</b></td><td><input type="text" name="title" size="30"></td></tr> 37 <tr><td><b>Page title:</b></td><td><input type="text" name="title" size="30"></td></tr>
14 <tr><td valign="top"><b>Update:</b></td><td> 38 <tr><td valign="top"><b>Update:</b></td><td>
15 <input type="checkbox" name="updateCheck" id="updateCheck" checked="checked"><label for="updateCheck">&nbsp;Notify me when a new release is ready</label></td> 39 <input type="checkbox" name="updateCheck" id="updateCheck" checked="checked"><label for="updateCheck">&nbsp;Notify me when a new release is ready</label></td>
diff --git a/tpl/vintage/js/shaarli.js b/tpl/vintage/js/shaarli.js
new file mode 100644
index 00000000..9bcc96fb
--- /dev/null
+++ b/tpl/vintage/js/shaarli.js
@@ -0,0 +1,32 @@
1window.onload = function () {
2 var continent = document.getElementById('continent');
3 var city = document.getElementById('city');
4 if (continent != null && city != null) {
5 continent.addEventListener('change', function(event) {
6 hideTimezoneCities(city, continent.options[continent.selectedIndex].value, true);
7 });
8 hideTimezoneCities(city, continent.options[continent.selectedIndex].value, false);
9 }
10};
11
12/**
13 * Add the class 'hidden' to city options not attached to the current selected continent.
14 *
15 * @param cities List of <option> elements
16 * @param currentContinent Current selected continent
17 * @param reset Set to true to reset the selected value
18 */
19function hideTimezoneCities(cities, currentContinent, reset = false) {
20 var first = true;
21 [].forEach.call(cities, function(option) {
22 if (option.getAttribute('data-continent') != currentContinent) {
23 option.className = 'hidden';
24 } else {
25 option.className = '';
26 if (reset === true && first === true) {
27 option.setAttribute('selected', 'selected');
28 first = false;
29 }
30 }
31 });
32}
diff --git a/tpl/vintage/page.footer.html b/tpl/vintage/page.footer.html
index 006d1d68..4ce0803a 100644
--- a/tpl/vintage/page.footer.html
+++ b/tpl/vintage/page.footer.html
@@ -26,6 +26,7 @@
26<script>function confirmDeleteLink() { var agree=confirm("Are you sure you want to delete this link ?"); if (agree) return true ; else return false ; }</script> 26<script>function confirmDeleteLink() { var agree=confirm("Are you sure you want to delete this link ?"); if (agree) return true ; else return false ; }</script>
27{/if} 27{/if}
28 28
29<script src="js/shaarli.js"></script>
29{loop="$plugins_footer.js_files"} 30{loop="$plugins_footer.js_files"}
30 <script src="{$value}#"></script> 31 <script src="{$value}#"></script>
31{/loop} 32{/loop}
diff --git a/tpl/vintage/page.header.html b/tpl/vintage/page.header.html
index cce61ec4..8a58844e 100644
--- a/tpl/vintage/page.header.html
+++ b/tpl/vintage/page.header.html
@@ -1,5 +1,5 @@
1 1
2<div id="logo" title="Share your links !" onclick="document.location='?';"></div> 2<div id="logo" title="Share your links !" onclick="document.location='{$titleLink}';"></div>
3 3
4<div id="linkcount" class="nomobile"> 4<div id="linkcount" class="nomobile">
5 {if="!empty($linkcount)"}{$linkcount} links{/if}<br> 5 {if="!empty($linkcount)"}{$linkcount} links{/if}<br>
@@ -16,7 +16,7 @@
16{if="!empty($_GET['source']) && $_GET['source']=='bookmarklet'"} 16{if="!empty($_GET['source']) && $_GET['source']=='bookmarklet'"}
17 {ignore} When called as a popup from bookmarklet, do not display menu. {/ignore} 17 {ignore} When called as a popup from bookmarklet, do not display menu. {/ignore}
18{else} 18{else}
19<li><a href="?" class="nomobile">Home</a></li> 19<li><a href="{$titleLink}" class="nomobile">Home</a></li>
20 {if="isLoggedIn()"} 20 {if="isLoggedIn()"}
21 <li><a href="?do=logout">Logout</a></li> 21 <li><a href="?do=logout">Logout</a></li>
22 <li><a href="?do=tools">Tools</a></li> 22 <li><a href="?do=tools">Tools</a></li>