]> git.immae.eu Git - github/shaarli/Shaarli.git/commitdiff
Merge pull request #856 from ArthurHoaro/api/delete-link
authorArthurHoaro <arthur@hoa.ro>
Sun, 7 May 2017 14:02:14 +0000 (16:02 +0200)
committerGitHub <noreply@github.com>
Sun, 7 May 2017 14:02:14 +0000 (16:02 +0200)
API: add DELETE endpoint

32 files changed:
application/FileUtils.php
application/History.php [new file with mode: 0644]
application/LinkDB.php
application/NetscapeBookmarkUtils.php
application/PageBuilder.php
application/TimeZone.php
application/Utils.php
application/config/ConfigManager.php
application/exceptions/IOException.php [new file with mode: 0644]
index.php
plugins/readityourself/book-open.png [deleted file]
plugins/readityourself/readityourself.html [deleted file]
plugins/readityourself/readityourself.meta [deleted file]
plugins/readityourself/readityourself.php [deleted file]
tests/FileUtilsTest.php [new file with mode: 0644]
tests/HistoryTest.php [new file with mode: 0644]
tests/LinkDBTest.php
tests/NetscapeBookmarkUtils/BookmarkImportTest.php
tests/TimeZoneTest.php
tests/UtilsTest.php
tests/plugins/PluginReadityourselfTest.php [deleted file]
tpl/default/configure.html
tpl/default/import.html
tpl/default/install.html
tpl/default/js/shaarli.js
tpl/vintage/configure.html
tpl/vintage/css/shaarli.css
tpl/vintage/import.html
tpl/vintage/install.html
tpl/vintage/js/shaarli.js [new file with mode: 0644]
tpl/vintage/page.footer.html
tpl/vintage/page.header.html

index 6cac9825de24a1ac4aec018cc54017e822064ef7..b8ad897029f7f0318feaf91fd76b728dc4d4ea12 100644 (file)
@@ -1,21 +1,76 @@
 <?php
+
+require_once 'exceptions/IOException.php';
+
 /**
- * Exception class thrown when a filesystem access failure happens
+ * Class FileUtils
+ *
+ * Utility class for file manipulation.
  */
-class IOException extends Exception
+class FileUtils
 {
-    private $path;
+    /**
+     * @var string
+     */
+    protected static $phpPrefix = '<?php /* ';
+
+    /**
+     * @var string
+     */
+    protected static $phpSuffix = ' */ ?>';
 
     /**
-     * Construct a new IOException
+     * Write data into a file (Shaarli database format).
+     * The data is stored in a PHP file, as a comment, in compressed base64 format.
+     *
+     * The file will be created if it doesn't exist.
+     *
+     * @param string $file    File path.
+     * @param string $content Content to write.
+     *
+     * @return int|bool Number of bytes written or false if it fails.
      *
-     * @param string $path    path to the resource that cannot be accessed
-     * @param string $message Custom exception message.
+     * @throws IOException The destination file can't be written.
      */
-    public function __construct($path, $message = '')
+    public static function writeFlatDB($file, $content)
     {
-        $this->path = $path;
-        $this->message = empty($message) ? 'Error accessing' : $message;
-        $this->message .= PHP_EOL . $this->path;
+        if (is_file($file) && !is_writeable($file)) {
+            // The datastore exists but is not writeable
+            throw new IOException($file);
+        } else if (!is_file($file) && !is_writeable(dirname($file))) {
+            // The datastore does not exist and its parent directory is not writeable
+            throw new IOException(dirname($file));
+        }
+
+        return file_put_contents(
+            $file,
+            self::$phpPrefix.base64_encode(gzdeflate(serialize($content))).self::$phpSuffix
+        );
+    }
+
+    /**
+     * Read data from a file containing Shaarli database format content.
+     * If the file isn't readable or doesn't exists, default data will be returned.
+     *
+     * @param string $file    File path.
+     * @param mixed  $default The default value to return if the file isn't readable.
+     *
+     * @return mixed The content unserialized, or default if the file isn't readable, or false if it fails.
+     */
+    public static function readFlatDB($file, $default = null)
+    {
+        // Note that gzinflate is faster than gzuncompress.
+        // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
+        if (is_readable($file)) {
+            return unserialize(
+                gzinflate(
+                    base64_decode(
+                        substr(file_get_contents($file), strlen(self::$phpPrefix), -strlen(self::$phpSuffix))
+                    )
+                )
+            );
+        }
+
+        return $default;
     }
 }
diff --git a/application/History.php b/application/History.php
new file mode 100644 (file)
index 0000000..f93b035
--- /dev/null
@@ -0,0 +1,200 @@
+<?php
+
+/**
+ * Class History
+ *
+ * Handle the history file tracing events in Shaarli.
+ * The history is stored as JSON in a file set by 'resource.history' setting.
+ *
+ * Available data:
+ *   - event: event key
+ *   - datetime: event date, in ISO8601 format.
+ *   - id: event item identifier (currently only link IDs).
+ *
+ * Available event keys:
+ *   - CREATED: new link
+ *   - UPDATED: link updated
+ *   - DELETED: link deleted
+ *   - SETTINGS: the settings have been updated through the UI.
+ *
+ * Note: new events are put at the beginning of the file and history array.
+ */
+class History
+{
+    /**
+     * @var string Action key: a new link has been created.
+     */
+    const CREATED = 'CREATED';
+
+    /**
+     * @var string Action key: a link has been updated.
+     */
+    const UPDATED = 'UPDATED';
+
+    /**
+     * @var string Action key: a link has been deleted.
+     */
+    const DELETED = 'DELETED';
+
+    /**
+     * @var string Action key: settings have been updated.
+     */
+    const SETTINGS = 'SETTINGS';
+
+    /**
+     * @var string History file path.
+     */
+    protected $historyFilePath;
+
+    /**
+     * @var array History data.
+     */
+    protected $history;
+
+    /**
+     * @var int History retention time in seconds (1 month).
+     */
+    protected $retentionTime = 2678400;
+
+    /**
+     * History constructor.
+     *
+     * @param string $historyFilePath History file path.
+     * @param int    $retentionTime   History content rentention time in seconds.
+     *
+     * @throws Exception if something goes wrong.
+     */
+    public function __construct($historyFilePath, $retentionTime = null)
+    {
+        $this->historyFilePath = $historyFilePath;
+        if ($retentionTime !== null) {
+            $this->retentionTime = $retentionTime;
+        }
+    }
+
+    /**
+     * Initialize: read history file.
+     *
+     * Allow lazy loading (don't read the file if it isn't necessary).
+     */
+    protected function initialize()
+    {
+        $this->check();
+        $this->read();
+    }
+
+    /**
+     * Add Event: new link.
+     *
+     * @param array $link Link data.
+     */
+    public function addLink($link)
+    {
+        $this->addEvent(self::CREATED, $link['id']);
+    }
+
+    /**
+     * Add Event: update existing link.
+     *
+     * @param array $link Link data.
+     */
+    public function updateLink($link)
+    {
+        $this->addEvent(self::UPDATED, $link['id']);
+    }
+
+    /**
+     * Add Event: delete existing link.
+     *
+     * @param array $link Link data.
+     */
+    public function deleteLink($link)
+    {
+        $this->addEvent(self::DELETED, $link['id']);
+    }
+
+    /**
+     * Add Event: settings updated.
+     */
+    public function updateSettings()
+    {
+        $this->addEvent(self::SETTINGS);
+    }
+
+    /**
+     * Save a new event and write it in the history file.
+     *
+     * @param string $status Event key, should be defined as constant.
+     * @param mixed  $id     Event item identifier (e.g. link ID).
+     */
+    protected function addEvent($status, $id = null)
+    {
+        if ($this->history === null) {
+            $this->initialize();
+        }
+
+        $item = [
+            'event' => $status,
+            'datetime' => (new DateTime())->format(DateTime::ATOM),
+            'id' => $id !== null ? $id : '',
+        ];
+        $this->history = array_merge([$item], $this->history);
+        $this->write();
+    }
+
+    /**
+     * Check that the history file is writable.
+     * Create the file if it doesn't exist.
+     *
+     * @throws Exception if it isn't writable.
+     */
+    protected function check()
+    {
+        if (! is_file($this->historyFilePath)) {
+            FileUtils::writeFlatDB($this->historyFilePath, []);
+        }
+
+        if (! is_writable($this->historyFilePath)) {
+            throw new Exception('History file isn\'t readable or writable');
+        }
+    }
+
+    /**
+     * Read JSON history file.
+     */
+    protected function read()
+    {
+        $this->history = FileUtils::readFlatDB($this->historyFilePath, []);
+        if ($this->history === false) {
+            throw new Exception('Could not parse history file');
+        }
+    }
+
+    /**
+     * Write JSON history file and delete old entries.
+     */
+    protected function write()
+    {
+        $comparaison = new DateTime('-'. $this->retentionTime . ' seconds');
+        foreach ($this->history as $key => $value) {
+            if (DateTime::createFromFormat(DateTime::ATOM, $value['datetime']) < $comparaison) {
+                unset($this->history[$key]);
+            }
+        }
+        FileUtils::writeFlatDB($this->historyFilePath, array_values($this->history));
+    }
+
+    /**
+     * Get the History.
+     *
+     * @return array
+     */
+    public function getHistory()
+    {
+        if ($this->history === null) {
+            $this->initialize();
+        }
+
+        return $this->history;
+    }
+}
index 4cee2af9942559fd122189687058ddddc1fc4ebf..0d3c85bd9815e2c879d8b18160441dfd19fd80f5 100644 (file)
@@ -50,12 +50,6 @@ class LinkDB implements Iterator, Countable, ArrayAccess
     // Link date storage format
     const LINK_DATE_FORMAT = 'Ymd_His';
 
-    // Datastore PHP prefix
-    protected static $phpPrefix = '<?php /* ';
-
-    // Datastore PHP suffix
-    protected static $phpSuffix = ' */ ?>';
-
     // List of links (associative array)
     //  - key:   link date (e.g. "20110823_124546"),
     //  - value: associative array (keys: title, description...)
@@ -144,10 +138,10 @@ class LinkDB implements Iterator, Countable, ArrayAccess
         if (!isset($value['id']) || empty($value['url'])) {
             die('Internal Error: A link should always have an id and URL.');
         }
-        if ((! empty($offset) && ! is_int($offset)) || ! is_int($value['id'])) {
+        if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) {
             die('You must specify an integer as a key.');
         }
-        if (! empty($offset) && $offset !== $value['id']) {
+        if ($offset !== null && $offset !== $value['id']) {
             die('Array offset and link ID must be equal.');
         }
 
@@ -295,16 +289,7 @@ You use the community supported version of the original Shaarli project, by Seba
             return;
         }
 
-        // Read data
-        // Note that gzinflate is faster than gzuncompress.
-        // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
-        $this->links = array();
-
-        if (file_exists($this->datastore)) {
-            $this->links = unserialize(gzinflate(base64_decode(
-                substr(file_get_contents($this->datastore),
-                       strlen(self::$phpPrefix), -strlen(self::$phpSuffix)))));
-        }
+        $this->links = FileUtils::readFlatDB($this->datastore, []);
 
         $toremove = array();
         foreach ($this->links as $key => &$link) {
@@ -361,19 +346,7 @@ You use the community supported version of the original Shaarli project, by Seba
      */
     private function write()
     {
-        if (is_file($this->datastore) && !is_writeable($this->datastore)) {
-            // The datastore exists but is not writeable
-            throw new IOException($this->datastore);
-        } else if (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) {
-            // The datastore does not exist and its parent directory is not writeable
-            throw new IOException(dirname($this->datastore));
-        }
-
-        file_put_contents(
-            $this->datastore,
-            self::$phpPrefix.base64_encode(gzdeflate(serialize($this->links))).self::$phpSuffix
-        );
-
+        FileUtils::writeFlatDB($this->datastore, $this->links);
     }
 
     /**
index ab346f81c2ada769d204ea79d8f5ed21efb90213..bbfde1386075c42b2deb61a57d8aaed1d39f336b 100644 (file)
@@ -95,10 +95,11 @@ class NetscapeBookmarkUtils
      * @param array         $files     Server $_FILES parameters
      * @param LinkDB        $linkDb    Loaded LinkDB instance
      * @param ConfigManager $conf      instance
+     * @param History       $history   History instance
      *
      * @return string Summary of the bookmark import status
      */
-    public static function import($post, $files, $linkDb, $conf)
+    public static function import($post, $files, $linkDb, $conf, $history)
     {
         $filename = $files['filetoupload']['name'];
         $filesize = $files['filetoupload']['size'];
@@ -182,6 +183,7 @@ class NetscapeBookmarkUtils
                 $linkDb[$existingLink['id']] = $newLink;
                 $importCount++;
                 $overwriteCount++;
+                $history->updateLink($newLink);
                 continue;
             }
 
@@ -193,6 +195,7 @@ class NetscapeBookmarkUtils
             $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']);
             $linkDb[$newLink['id']] = $newLink;
             $importCount++;
+            $history->addLink($newLink);
         }
 
         $linkDb->save($conf->get('resource.page_cache'));
index b133dee83644794f7617726a79e1d087f373f825..8e39455bffe443b975be7d376628e66382e7b8bc 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 
+use Shaarli\Config\ConfigManager;
+
 /**
  * This class is in charge of building the final page.
  * (This is basically a wrapper around RainTPL which pre-fills some fields.)
index 36a8fb122ada91a1832f25944895f8aeca599288..c1869ef87e1d0b96b105c792d8dc902c15ca5246 100644 (file)
@@ -1,23 +1,42 @@
 <?php
 /**
- * Generates the timezone selection form and JavaScript.
+ * Generates a list of available timezone continents and cities.
  *
- * Note: 'UTC/UTC' is mapped to 'UTC' to form a valid option
+ * Two distinct array based on available timezones
+ * and the one selected in the settings:
+ *   - (0) continents:
+ *     + list of available continents
+ *     + special key 'selected' containing the value of the selected timezone's continent
+ *   - (1) cities:
+ *     + list of available cities associated with their continent
+ *     + special key 'selected' containing the value of the selected timezone's city (without the continent)
  *
- * Example: preselect Europe/Paris
- *  list($htmlform, $js) = generateTimeZoneForm('Europe/Paris');
+ * Example:
+ *   [
+ *     [
+ *       'America',
+ *       'Europe',
+ *       'selected' => 'Europe',
+ *     ],
+ *     [
+ *       ['continent' => 'America', 'city' => 'Toronto'],
+ *       ['continent' => 'Europe', 'city' => 'Paris'],
+ *       'selected' => 'Paris',
+ *     ],
+ *   ];
  *
+ * Notes:
+ *   - 'UTC/UTC' is mapped to 'UTC' to form a valid option
+ *   - a few timezone cities includes the country/state, such as Argentina/Buenos_Aires
+ *   - these arrays are designed to build timezone selects in template files with any HTML structure
+ *
+ * @param array  $installedTimeZones  List of installed timezones as string
  * @param string $preselectedTimezone preselected timezone (optional)
  *
- * @return array containing the generated HTML form and Javascript code
+ * @return array[] continents and cities
  **/
-function generateTimeZoneForm($preselectedTimezone='')
+function generateTimeZoneData($installedTimeZones, $preselectedTimezone = '')
 {
-    // Select the server timezone
-    if ($preselectedTimezone == '') {
-        $preselectedTimezone = date_default_timezone_get();
-    }
-
     if ($preselectedTimezone == 'UTC') {
         $pcity = $pcontinent = 'UTC';
     } else {
@@ -27,62 +46,30 @@ function generateTimeZoneForm($preselectedTimezone='')
         $pcity = substr($preselectedTimezone, $spos+1);
     }
 
-    // The list is in the form 'Europe/Paris', 'America/Argentina/Buenos_Aires'
-    // We split the list in continents/cities.
-    $continents = array();
-    $cities = array();
-
-    // TODO: use a template to generate the HTML/Javascript form
-
-    foreach (timezone_identifiers_list() as $tz) {
+    $continents = [];
+    $cities = [];
+    foreach ($installedTimeZones as $tz) {
         if ($tz == 'UTC') {
             $tz = 'UTC/UTC';
         }
         $spos = strpos($tz, '/');
 
-        if ($spos !== false) {
-            $continent = substr($tz, 0, $spos);
-            $city = substr($tz, $spos+1);
-            $continents[$continent] = 1;
-
-            if (!isset($cities[$continent])) {
-                $cities[$continent] = '';
-            }
-            $cities[$continent] .= '<option value="'.$city.'"';
-            if ($pcity == $city) {
-                $cities[$continent] .= ' selected="selected"';
-            }
-            $cities[$continent] .= '>'.$city.'</option>';
+        // Ignore invalid timezones
+        if ($spos === false) {
+            continue;
         }
-    }
-
-    $continentsHtml = '';
-    $continents = array_keys($continents);
 
-    foreach ($continents as $continent) {
-        $continentsHtml .= '<option  value="'.$continent.'"';
-        if ($pcontinent == $continent) {
-            $continentsHtml .= ' selected="selected"';
-        }
-        $continentsHtml .= '>'.$continent.'</option>';
+        $continent = substr($tz, 0, $spos);
+        $city = substr($tz, $spos+1);
+        $cities[] = ['continent' => $continent, 'city' => $city];
+        $continents[$continent] = true;
     }
 
-    // Timezone selection form
-    $timezoneForm = 'Continent:';
-    $timezoneForm .= '<select name="continent" id="continent" onChange="onChangecontinent();">';
-    $timezoneForm .= $continentsHtml.'</select>';
-    $timezoneForm .= '&nbsp;&nbsp;&nbsp;&nbsp;City:';
-    $timezoneForm .= '<select name="city" id="city">'.$cities[$pcontinent].'</select><br />';
-
-    // Javascript handler - updates the city list when the user selects a continent
-    $timezoneJs = '<script>';
-    $timezoneJs .= 'function onChangecontinent() {';
-    $timezoneJs .= 'document.getElementById("city").innerHTML =';
-    $timezoneJs .= ' citiescontinent[document.getElementById("continent").value]; }';
-    $timezoneJs .= 'var citiescontinent = '.json_encode($cities).';';
-    $timezoneJs .= '</script>';
+    $continents = array_keys($continents);
+    $continents['selected'] = $pcontinent;
+    $cities['selected'] = $pcity;
 
-    return array($timezoneForm, $timezoneJs);
+    return [$continents, $cities];
 }
 
 /**
index d6e066102b62a61baf96c4ad5cecd947725a11ac..ab463af9749cf3a597305fa37e1e91dbcf26046f 100644 (file)
@@ -345,3 +345,93 @@ function format_date($date, $time = true, $intl = true)
 
     return $formatter->format($date);
 }
+
+/**
+ * Check if the input is an integer, no matter its real type.
+ *
+ * PHP is a bit messy regarding this:
+ *   - is_int returns false if the input is a string
+ *   - ctype_digit returns false if the input is an integer or negative
+ *
+ * @param mixed $input value
+ *
+ * @return bool true if the input is an integer, false otherwise
+ */
+function is_integer_mixed($input)
+{
+    if (is_array($input) || is_bool($input) || is_object($input)) {
+        return false;
+    }
+    $input = strval($input);
+    return ctype_digit($input) || (startsWith($input, '-') && ctype_digit(substr($input, 1)));
+}
+
+/**
+ * Convert post_max_size/upload_max_filesize (e.g. '16M') parameters to bytes.
+ *
+ * @param string $val Size expressed in string.
+ *
+ * @return int Size expressed in bytes.
+ */
+function return_bytes($val)
+{
+    if (is_integer_mixed($val) || $val === '0' || empty($val)) {
+        return $val;
+    }
+    $val = trim($val);
+    $last = strtolower($val[strlen($val)-1]);
+    $val = intval(substr($val, 0, -1));
+    switch($last) {
+        case 'g': $val *= 1024;
+        case 'm': $val *= 1024;
+        case 'k': $val *= 1024;
+    }
+    return $val;
+}
+
+/**
+ * Return a human readable size from bytes.
+ *
+ * @param int $bytes value
+ *
+ * @return string Human readable size
+ */
+function human_bytes($bytes)
+{
+    if ($bytes === '') {
+        return t('Setting not set');
+    }
+    if (! is_integer_mixed($bytes)) {
+        return $bytes;
+    }
+    $bytes = intval($bytes);
+    if ($bytes === 0) {
+        return t('Unlimited');
+    }
+
+    $units = [t('B'), t('kiB'), t('MiB'), t('GiB')];
+    for ($i = 0; $i < count($units) && $bytes >= 1024; ++$i) {
+        $bytes /= 1024;
+    }
+
+    return round($bytes) . $units[$i];
+}
+
+/**
+ * Try to determine max file size for uploads (POST).
+ * Returns an integer (in bytes) or formatted depending on $format.
+ *
+ * @param mixed $limitPost   post_max_size PHP setting
+ * @param mixed $limitUpload upload_max_filesize PHP setting
+ * @param bool  $format      Format max upload size to human readable size
+ *
+ * @return int|string max upload file size
+ */
+function get_max_upload_size($limitPost, $limitUpload, $format = true)
+{
+    $size1 = return_bytes($limitPost);
+    $size2 = return_bytes($limitUpload);
+    // Return the smaller of two:
+    $maxsize = min($size1, $size2);
+    return $format ? human_bytes($maxsize) : $maxsize;
+}
index 7bfbfc729fe4b67c153d8c5c367d1bd763d074f5..86a917fb12c2fa0c23c5a4a83c99e4ab4afb44bd 100644 (file)
@@ -301,6 +301,7 @@ class ConfigManager
         $this->setEmpty('resource.updates', 'data/updates.txt');
         $this->setEmpty('resource.log', 'data/log.txt');
         $this->setEmpty('resource.update_check', 'data/lastupdatecheck.txt');
+        $this->setEmpty('resource.history', 'data/history.php');
         $this->setEmpty('resource.raintpl_tpl', 'tpl/');
         $this->setEmpty('resource.theme', 'default');
         $this->setEmpty('resource.raintpl_tmp', 'tmp/');
diff --git a/application/exceptions/IOException.php b/application/exceptions/IOException.php
new file mode 100644 (file)
index 0000000..b563b23
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * Exception class thrown when a filesystem access failure happens
+ */
+class IOException extends Exception
+{
+    private $path;
+
+    /**
+     * Construct a new IOException
+     *
+     * @param string $path    path to the resource that cannot be accessed
+     * @param string $message Custom exception message.
+     */
+    public function __construct($path, $message = '')
+    {
+        $this->path = $path;
+        $this->message = empty($message) ? 'Error accessing' : $message;
+        $this->message .= ' "' . $this->path .'"';
+    }
+}
index 6eaa56c210931d5e5afcb296e71e51fc1614ae66..02fe2577c77ad50c53f11f7ac4fbbdcc8db1bde3 100644 (file)
--- a/index.php
+++ b/index.php
@@ -62,6 +62,7 @@ require_once 'application/CachedPage.php';
 require_once 'application/config/ConfigPlugin.php';
 require_once 'application/FeedBuilder.php';
 require_once 'application/FileUtils.php';
+require_once 'application/History.php';
 require_once 'application/HttpUtils.php';
 require_once 'application/Languages.php';
 require_once 'application/LinkDB.php';
@@ -472,34 +473,6 @@ if (isset($_POST['login']))
     }
 }
 
-// ------------------------------------------------------------------------------------------
-// Misc utility functions:
-
-// Convert post_max_size/upload_max_filesize (e.g. '16M') parameters to bytes.
-function return_bytes($val)
-{
-    $val = trim($val); $last=strtolower($val[strlen($val)-1]);
-    switch($last)
-    {
-        case 'g': $val *= 1024;
-        case 'm': $val *= 1024;
-        case 'k': $val *= 1024;
-    }
-    return $val;
-}
-
-// Try to determine max file size for uploads (POST).
-// Returns an integer (in bytes)
-function getMaxFileSize()
-{
-    $size1 = return_bytes(ini_get('post_max_size'));
-    $size2 = return_bytes(ini_get('upload_max_filesize'));
-    // Return the smaller of two:
-    $maxsize = min($size1,$size2);
-    // FIXME: Then convert back to readable notations ? (e.g. 2M instead of 2000000)
-    return $maxsize;
-}
-
 // ------------------------------------------------------------------------------------------
 // Token management for XSRF protection
 // Token should be used in any form which acts on data (create,update,delete,import...).
@@ -755,6 +728,12 @@ function renderPage($conf, $pluginManager, $LINKSDB)
         die($e->getMessage());
     }
 
+    try {
+        $history = new History($conf->get('resource.history'));
+    } catch(Exception $e) {
+        die($e->getMessage());
+    }
+
     $PAGE = new PageBuilder($conf);
     $PAGE->assign('linkcount', count($LINKSDB));
     $PAGE->assign('privateLinkcount', count_private($LINKSDB));
@@ -1153,6 +1132,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
             $conf->set('api.secret', escape($_POST['apiSecret']));
             try {
                 $conf->write(isLoggedIn());
+                $history->updateSettings();
                 invalidateCaches($conf->get('resource.page_cache'));
             }
             catch(Exception $e) {
@@ -1174,9 +1154,12 @@ function renderPage($conf, $pluginManager, $LINKSDB)
             $PAGE->assign('theme', $conf->get('resource.theme'));
             $PAGE->assign('theme_available', ThemeUtils::getThemes($conf->get('resource.raintpl_tpl')));
             $PAGE->assign('redirector', $conf->get('redirector.url'));
-            list($timezone_form, $timezone_js) = generateTimeZoneForm($conf->get('general.timezone'));
-            $PAGE->assign('timezone_form', $timezone_form);
-            $PAGE->assign('timezone_js',$timezone_js);
+            list($continents, $cities) = generateTimeZoneData(
+                timezone_identifiers_list(),
+                $conf->get('general.timezone')
+            );
+            $PAGE->assign('continents', $continents);
+            $PAGE->assign('cities', $cities);
             $PAGE->assign('private_links_default', $conf->get('privacy.default_private_links', false));
             $PAGE->assign('session_protection_disabled', $conf->get('security.session_protection_disabled', false));
             $PAGE->assign('enable_rss_permalinks', $conf->get('feed.rss_permalinks', false));
@@ -1184,6 +1167,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
             $PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false));
             $PAGE->assign('api_enabled', $conf->get('api.enabled', true));
             $PAGE->assign('api_secret', $conf->get('api.secret'));
+            $history->updateSettings();
             $PAGE->renderPage('configure');
             exit;
         }
@@ -1213,6 +1197,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
                 unset($tags[array_search($needle,$tags)]); // Remove tag.
                 $value['tags']=trim(implode(' ',$tags));
                 $LINKSDB[$key]=$value;
+                $history->updateLink($LINKSDB[$key]);
             }
             $LINKSDB->save($conf->get('resource.page_cache'));
             echo '<script>alert("Tag was removed from '.count($linksToAlter).' links.");document.location=\'?do=changetag\';</script>';
@@ -1230,6 +1215,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
                 $tags[array_search($needle, $tags)] = trim($_POST['totag']);
                 $value['tags'] = implode(' ', array_unique($tags));
                 $LINKSDB[$key] = $value;
+                $history->updateLink($LINKSDB[$key]);
             }
             $LINKSDB->save($conf->get('resource.page_cache')); // Save to disk.
             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)
             $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate);
             $updated = new DateTime();
             $shortUrl = $LINKSDB[$id]['shorturl'];
+            $new = false;
         } else {
             // New link
             $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate);
             $updated = null;
             $shortUrl = link_small_hash($created, $id);
+            $new = true;
         }
 
         // Remove multiple spaces.
@@ -1307,6 +1295,11 @@ function renderPage($conf, $pluginManager, $LINKSDB)
 
         $LINKSDB[$id] = $link;
         $LINKSDB->save($conf->get('resource.page_cache'));
+        if ($new) {
+            $history->addLink($link);
+        } else {
+            $history->updateLink($link);
+        }
 
         // If we are called from the bookmarklet, we must close the popup:
         if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
@@ -1357,6 +1350,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
         $pluginManager->executeHooks('delete_link', $link);
         unset($LINKSDB[$id]);
         $LINKSDB->save($conf->get('resource.page_cache')); // save to disk
+        $history->deleteLink($link);
 
         // If we are called from the bookmarklet, we must close the popup:
         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)
 
         if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) {
             // Show import dialog
-            $PAGE->assign('maxfilesize', getMaxFileSize());
+            $PAGE->assign(
+                'maxfilesize',
+                get_max_upload_size(
+                    ini_get('post_max_size'),
+                    ini_get('upload_max_filesize'),
+                    false
+                )
+            );
+            $PAGE->assign(
+                'maxfilesizeHuman',
+                get_max_upload_size(
+                    ini_get('post_max_size'),
+                    ini_get('upload_max_filesize'),
+                    true
+                )
+            );
             $PAGE->renderPage('import');
             exit;
         }
@@ -1527,7 +1536,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
             // The file is too big or some form field may be missing.
             echo '<script>alert("The file you are trying to upload is probably'
                 .' bigger than what this webserver can accept ('
-                .getMaxFileSize().' bytes).'
+                .get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')).').'
                 .' Please upload in smaller chunks.");document.location=\'?do='
                 .Router::$PAGE_IMPORT .'\';</script>';
             exit;
@@ -1539,7 +1548,8 @@ function renderPage($conf, $pluginManager, $LINKSDB)
             $_POST,
             $_FILES,
             $LINKSDB,
-            $conf
+            $conf,
+            $history
         );
         echo '<script>alert("'.$status.'");document.location=\'?do='
              .Router::$PAGE_IMPORT .'\';</script>';
@@ -1568,6 +1578,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
 
     // Plugin administration form action
     if ($targetPage == Router::$PAGE_SAVE_PLUGINSADMIN) {
+        $history->updateSettings();
         try {
             if (isset($_POST['parameters_form'])) {
                 unset($_POST['parameters_form']);
@@ -1982,16 +1993,10 @@ function install($conf)
         exit;
     }
 
-    // Display config form:
-    list($timezone_form, $timezone_js) = generateTimeZoneForm();
-    $timezone_html = '';
-    if ($timezone_form != '') {
-        $timezone_html = '<tr><td><b>Timezone:</b></td><td>'.$timezone_form.'</td></tr>';
-    }
-
     $PAGE = new PageBuilder($conf);
-    $PAGE->assign('timezone_html',$timezone_html);
-    $PAGE->assign('timezone_js',$timezone_js);
+    list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
+    $PAGE->assign('continents', $continents);
+    $PAGE->assign('cities', $cities);
     $PAGE->renderPage('install');
     exit;
 }
diff --git a/plugins/readityourself/book-open.png b/plugins/readityourself/book-open.png
deleted file mode 100644 (file)
index 36513d7..0000000
Binary files a/plugins/readityourself/book-open.png and /dev/null differ
diff --git a/plugins/readityourself/readityourself.html b/plugins/readityourself/readityourself.html
deleted file mode 100644 (file)
index 5e20071..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<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 (file)
index bd611dd..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-description="For each link, add a ReadItYourself icon to save the shaared URL."
-parameters=READITYOUSELF_URL;
\ No newline at end of file
diff --git a/plugins/readityourself/readityourself.php b/plugins/readityourself/readityourself.php
deleted file mode 100644 (file)
index 961c5bd..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php
-
-/**
- * Plugin readityourself
- */
-
-// If we're talking about https://github.com/memiks/readityourself
-// it seems kinda dead.
-// Not tested.
-
-/**
- * Init function, return an error if the server is not set.
- *
- * @param $conf ConfigManager instance.
- *
- * @return array Eventual error.
- */
-function readityourself_init($conf)
-{
-    $riyUrl = $conf->get('plugins.READITYOUSELF_URL');
-    if (empty($riyUrl)) {
-        $error = 'Readityourself plugin error: '.
-            'Please define the "READITYOUSELF_URL" setting in the plugin administration page.';
-        return array($error);
-    }
-}
-
-/**
- * Add readityourself icon to link_plugin when rendering linklist.
- *
- * @param mixed         $data Linklist data.
- * @param ConfigManager $conf Configuration Manager instance.
- *
- * @return mixed - linklist data with readityourself plugin.
- */
-function hook_readityourself_render_linklist($data, $conf)
-{
-    $riyUrl = $conf->get('plugins.READITYOUSELF_URL');
-    if (empty($riyUrl)) {
-        return $data;
-    }
-
-    $readityourself_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/readityourself/readityourself.html');
-
-    foreach ($data['links'] as &$value) {
-        $readityourself = sprintf($readityourself_html, $riyUrl, $value['url'], PluginManager::$PLUGINS_PATH);
-        $value['link_plugin'][] = $readityourself;
-    }
-
-    return $data;
-}
diff --git a/tests/FileUtilsTest.php b/tests/FileUtilsTest.php
new file mode 100644 (file)
index 0000000..d764e49
--- /dev/null
@@ -0,0 +1,108 @@
+<?php
+
+require_once 'application/FileUtils.php';
+
+/**
+ * Class FileUtilsTest
+ *
+ * Test file utility class.
+ */
+class FileUtilsTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * @var string Test file path.
+     */
+    protected static $file = 'sandbox/flat.db';
+
+    /**
+     * Delete test file after every test.
+     */
+    public function tearDown()
+    {
+        @unlink(self::$file);
+    }
+
+    /**
+     * Test writeDB, then readDB with different data.
+     */
+    public function testSimpleWriteRead()
+    {
+        $data = ['blue', 'red'];
+        $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0);
+        $this->assertTrue(startsWith(file_get_contents(self::$file), '<?php /*'));
+        $this->assertEquals($data, FileUtils::readFlatDB(self::$file));
+
+        $data = 0;
+        $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0);
+        $this->assertEquals($data, FileUtils::readFlatDB(self::$file));
+
+        $data = null;
+        $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0);
+        $this->assertEquals($data, FileUtils::readFlatDB(self::$file));
+
+        $data = false;
+        $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0);
+        $this->assertEquals($data, FileUtils::readFlatDB(self::$file));
+    }
+
+    /**
+     * File not writable: raise an exception.
+     *
+     * @expectedException IOException
+     * @expectedExceptionMessage Error accessing "sandbox/flat.db"
+     */
+    public function testWriteWithoutPermission()
+    {
+        touch(self::$file);
+        chmod(self::$file, 0440);
+        FileUtils::writeFlatDB(self::$file, null);
+    }
+
+    /**
+     * Folder non existent: raise an exception.
+     *
+     * @expectedException IOException
+     * @expectedExceptionMessage Error accessing "nopefolder"
+     */
+    public function testWriteFolderDoesNotExist()
+    {
+        FileUtils::writeFlatDB('nopefolder/file', null);
+    }
+
+    /**
+     * Folder non writable: raise an exception.
+     *
+     * @expectedException IOException
+     * @expectedExceptionMessage Error accessing "sandbox"
+     */
+    public function testWriteFolderPermission()
+    {
+        chmod(dirname(self::$file), 0555);
+        try {
+            FileUtils::writeFlatDB(self::$file, null);
+        } catch (Exception $e) {
+            chmod(dirname(self::$file), 0755);
+            throw $e;
+        }
+    }
+
+    /**
+     * Read non existent file, use default parameter.
+     */
+    public function testReadNotExistentFile()
+    {
+        $this->assertEquals(null, FileUtils::readFlatDB(self::$file));
+        $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test']));
+    }
+
+    /**
+     * Read non readable file, use default parameter.
+     */
+    public function testReadNotReadable()
+    {
+        touch(self::$file);
+        chmod(self::$file, 0220);
+        $this->assertEquals(null, FileUtils::readFlatDB(self::$file));
+        $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test']));
+    }
+}
diff --git a/tests/HistoryTest.php b/tests/HistoryTest.php
new file mode 100644 (file)
index 0000000..9152584
--- /dev/null
@@ -0,0 +1,207 @@
+<?php
+
+require_once 'application/History.php';
+
+
+class HistoryTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * @var string History file path
+     */
+    protected static $historyFilePath = 'sandbox/history.php';
+
+    /**
+     * Delete history file.
+     */
+    public function tearDown()
+    {
+        @unlink(self::$historyFilePath);
+    }
+
+    /**
+     * Test that the history file is created if it doesn't exist.
+     */
+    public function testConstructLazyLoading()
+    {
+        new History(self::$historyFilePath);
+        $this->assertFileNotExists(self::$historyFilePath);
+    }
+
+    /**
+     * Test that the history file is created if it doesn't exist.
+     */
+    public function testAddEventCreateFile()
+    {
+        $history = new History(self::$historyFilePath);
+        $history->updateSettings();
+        $this->assertFileExists(self::$historyFilePath);
+    }
+
+    /**
+     * Not writable history file: raise an exception.
+     *
+     * @expectedException Exception
+     * @expectedExceptionMessage History file isn't readable or writable
+     */
+    public function testConstructNotWritable()
+    {
+        touch(self::$historyFilePath);
+        chmod(self::$historyFilePath, 0440);
+        $history = new History(self::$historyFilePath);
+        $history->updateSettings();
+    }
+
+    /**
+     * Not parsable history file: raise an exception.
+     *
+     * @expectedException Exception
+     * @expectedExceptionMessageRegExp /Could not parse history file/
+     */
+    public function testConstructNotParsable()
+    {
+        file_put_contents(self::$historyFilePath, 'not parsable');
+        $history = new History(self::$historyFilePath);
+        // gzinflate generates a warning
+        @$history->updateSettings();
+    }
+
+    /**
+     * Test add link event
+     */
+    public function testAddLink()
+    {
+        $history = new History(self::$historyFilePath);
+        $history->addLink(['id' => 0]);
+        $actual = $history->getHistory()[0];
+        $this->assertEquals(History::CREATED, $actual['event']);
+        $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
+        $this->assertEquals(0, $actual['id']);
+
+        $history = new History(self::$historyFilePath);
+        $history->addLink(['id' => 1]);
+        $actual = $history->getHistory()[0];
+        $this->assertEquals(History::CREATED, $actual['event']);
+        $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
+        $this->assertEquals(1, $actual['id']);
+
+        $history = new History(self::$historyFilePath);
+        $history->addLink(['id' => 'str']);
+        $actual = $history->getHistory()[0];
+        $this->assertEquals(History::CREATED, $actual['event']);
+        $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
+        $this->assertEquals('str', $actual['id']);
+    }
+
+    /**
+     * Test updated link event
+     */
+    public function testUpdateLink()
+    {
+        $history = new History(self::$historyFilePath);
+        $history->updateLink(['id' => 1]);
+        $actual = $history->getHistory()[0];
+        $this->assertEquals(History::UPDATED, $actual['event']);
+        $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
+        $this->assertEquals(1, $actual['id']);
+    }
+
+    /**
+     * Test delete link event
+     */
+    public function testDeleteLink()
+    {
+        $history = new History(self::$historyFilePath);
+        $history->deleteLink(['id' => 1]);
+        $actual = $history->getHistory()[0];
+        $this->assertEquals(History::DELETED, $actual['event']);
+        $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
+        $this->assertEquals(1, $actual['id']);
+    }
+
+    /**
+     * Test updated settings event
+     */
+    public function testUpdateSettings()
+    {
+        $history = new History(self::$historyFilePath);
+        $history->updateSettings();
+        $actual = $history->getHistory()[0];
+        $this->assertEquals(History::SETTINGS, $actual['event']);
+        $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
+        $this->assertEmpty($actual['id']);
+    }
+
+    /**
+     * Make sure that new items are stored at the beginning
+     */
+    public function testHistoryOrder()
+    {
+        $history = new History(self::$historyFilePath);
+        $history->updateLink(['id' => 1]);
+        $actual = $history->getHistory()[0];
+        $this->assertEquals(History::UPDATED, $actual['event']);
+        $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
+        $this->assertEquals(1, $actual['id']);
+
+        $history->addLink(['id' => 1]);
+        $actual = $history->getHistory()[0];
+        $this->assertEquals(History::CREATED, $actual['event']);
+        $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
+        $this->assertEquals(1, $actual['id']);
+    }
+
+    /**
+     * Re-read history from file after writing an event
+     */
+    public function testHistoryRead()
+    {
+        $history = new History(self::$historyFilePath);
+        $history->updateLink(['id' => 1]);
+        $history = new History(self::$historyFilePath);
+        $actual = $history->getHistory()[0];
+        $this->assertEquals(History::UPDATED, $actual['event']);
+        $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
+        $this->assertEquals(1, $actual['id']);
+    }
+
+    /**
+     * Re-read history from file after writing an event and make sure that the order is correct
+     */
+    public function testHistoryOrderRead()
+    {
+        $history = new History(self::$historyFilePath);
+        $history->updateLink(['id' => 1]);
+        $history->addLink(['id' => 1]);
+
+        $history = new History(self::$historyFilePath);
+        $actual = $history->getHistory()[0];
+        $this->assertEquals(History::CREATED, $actual['event']);
+        $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
+        $this->assertEquals(1, $actual['id']);
+
+        $actual = $history->getHistory()[1];
+        $this->assertEquals(History::UPDATED, $actual['event']);
+        $this->assertTrue(new DateTime('-2 seconds') < DateTime::createFromFormat(DateTime::ATOM, $actual['datetime']));
+        $this->assertEquals(1, $actual['id']);
+    }
+
+    /**
+     * Test retention time: delete old entries.
+     */
+    public function testHistoryRententionTime()
+    {
+        $history = new History(self::$historyFilePath, 5);
+        $history->updateLink(['id' => 1]);
+        $this->assertEquals(1, count($history->getHistory()));
+        $arr = $history->getHistory();
+        $arr[0]['datetime'] = (new DateTime('-1 hour'))->format(DateTime::ATOM);
+        FileUtils::writeFlatDB(self::$historyFilePath, $arr);
+
+        $history = new History(self::$historyFilePath, 60);
+        $this->assertEquals(1, count($history->getHistory()));
+        $this->assertEquals(1, $history->getHistory()[0]['id']);
+        $history->updateLink(['id' => 2]);
+        $this->assertEquals(1, count($history->getHistory()));
+        $this->assertEquals(2, $history->getHistory()[0]['id']);
+    }
+}
index 1f62a34a160ea5e17c31bbaa0117ef7785ff4202..7bf98f92101d572de71e345bf55080457b5aae7f 100644 (file)
@@ -101,7 +101,7 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
      * Attempt to instantiate a LinkDB whereas the datastore is not writable
      *
      * @expectedException              IOException
-     * @expectedExceptionMessageRegExp /Error accessing\nnull/
+     * @expectedExceptionMessageRegExp /Error accessing "null"/
      */
     public function testConstructDatastoreNotWriteable()
     {
index 5925a8e156865be6c3343ca0e6b937a08cb97cf0..f838f2594793a40be4e0110a20c4160d41c6b363 100644 (file)
@@ -33,6 +33,11 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
      */
     protected static $testDatastore = 'sandbox/datastore.php';
 
+    /**
+     * @var string History file path
+     */
+    protected static $historyFilePath = 'sandbox/history.php';
+
     /**
      * @var LinkDB private LinkDB instance
      */
@@ -48,6 +53,11 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
      */
     protected $conf;
 
+    /**
+     * @var History instance.
+     */
+    protected $history;
+
     /**
      * @var string Save the current timezone.
      */
@@ -73,6 +83,15 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->linkDb = new LinkDB(self::$testDatastore, true, false);
         $this->conf = new ConfigManager('tests/utils/config/configJson');
         $this->conf->set('resource.page_cache', $this->pagecache);
+        $this->history = new History(self::$historyFilePath);
+    }
+
+    /**
+     * Delete history file.
+     */
+    public function tearDown()
+    {
+        @unlink(self::$historyFilePath);
     }
 
     public static function tearDownAfterClass()
@@ -89,7 +108,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(
             'File empty.htm (0 bytes) has an unknown file format.'
             .' Nothing was imported.',
-            NetscapeBookmarkUtils::import(NULL, $files, NULL, $this->conf)
+            NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history)
         );
         $this->assertEquals(0, count($this->linkDb));
     }
@@ -102,7 +121,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $files = file2array('no_doctype.htm');
         $this->assertEquals(
             'File no_doctype.htm (350 bytes) has an unknown file format. Nothing was imported.',
-            NetscapeBookmarkUtils::import(NULL, $files, NULL, $this->conf)
+            NetscapeBookmarkUtils::import(null, $files, null, $this->conf, $this->history)
         );
         $this->assertEquals(0, count($this->linkDb));
     }
@@ -116,7 +135,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(
             'File internet_explorer_encoding.htm (356 bytes) was successfully processed:'
             .' 1 links imported, 0 links overwritten, 0 links skipped.',
-            NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf)
+            NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
         );
         $this->assertEquals(1, count($this->linkDb));
         $this->assertEquals(0, count_private($this->linkDb));
@@ -145,7 +164,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(
             'File netscape_nested.htm (1337 bytes) was successfully processed:'
             .' 8 links imported, 0 links overwritten, 0 links skipped.',
-            NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf)
+            NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
         );
         $this->assertEquals(8, count($this->linkDb));
         $this->assertEquals(2, count_private($this->linkDb));
@@ -267,7 +286,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(
             'File netscape_basic.htm (482 bytes) was successfully processed:'
             .' 2 links imported, 0 links overwritten, 0 links skipped.',
-            NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf)
+            NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
         );
 
         $this->assertEquals(2, count($this->linkDb));
@@ -312,7 +331,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(
             'File netscape_basic.htm (482 bytes) was successfully processed:'
             .' 2 links imported, 0 links overwritten, 0 links skipped.',
-            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf)
+            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
         $this->assertEquals(2, count($this->linkDb));
         $this->assertEquals(1, count_private($this->linkDb));
@@ -356,7 +375,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(
             'File netscape_basic.htm (482 bytes) was successfully processed:'
             .' 2 links imported, 0 links overwritten, 0 links skipped.',
-            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf)
+            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
         $this->assertEquals(2, count($this->linkDb));
         $this->assertEquals(0, count_private($this->linkDb));
@@ -380,7 +399,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(
             'File netscape_basic.htm (482 bytes) was successfully processed:'
             .' 2 links imported, 0 links overwritten, 0 links skipped.',
-            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf)
+            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
         $this->assertEquals(2, count($this->linkDb));
         $this->assertEquals(2, count_private($this->linkDb));
@@ -406,7 +425,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(
             'File netscape_basic.htm (482 bytes) was successfully processed:'
             .' 2 links imported, 0 links overwritten, 0 links skipped.',
-            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf)
+            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
         $this->assertEquals(2, count($this->linkDb));
         $this->assertEquals(2, count_private($this->linkDb));
@@ -426,7 +445,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(
             'File netscape_basic.htm (482 bytes) was successfully processed:'
             .' 2 links imported, 2 links overwritten, 0 links skipped.',
-            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf)
+            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
         $this->assertEquals(2, count($this->linkDb));
         $this->assertEquals(0, count_private($this->linkDb));
@@ -452,7 +471,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(
             'File netscape_basic.htm (482 bytes) was successfully processed:'
             .' 2 links imported, 0 links overwritten, 0 links skipped.',
-            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf)
+            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
         $this->assertEquals(2, count($this->linkDb));
         $this->assertEquals(0, count_private($this->linkDb));
@@ -473,7 +492,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(
             'File netscape_basic.htm (482 bytes) was successfully processed:'
             .' 2 links imported, 2 links overwritten, 0 links skipped.',
-            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf)
+            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
         $this->assertEquals(2, count($this->linkDb));
         $this->assertEquals(2, count_private($this->linkDb));
@@ -497,7 +516,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(
             'File netscape_basic.htm (482 bytes) was successfully processed:'
             .' 2 links imported, 0 links overwritten, 0 links skipped.',
-            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf)
+            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
         $this->assertEquals(2, count($this->linkDb));
         $this->assertEquals(0, count_private($this->linkDb));
@@ -507,7 +526,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(
             'File netscape_basic.htm (482 bytes) was successfully processed:'
             .' 0 links imported, 0 links overwritten, 2 links skipped.',
-            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf)
+            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
         $this->assertEquals(2, count($this->linkDb));
         $this->assertEquals(0, count_private($this->linkDb));
@@ -526,7 +545,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(
             'File netscape_basic.htm (482 bytes) was successfully processed:'
             .' 2 links imported, 0 links overwritten, 0 links skipped.',
-            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf)
+            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
         $this->assertEquals(2, count($this->linkDb));
         $this->assertEquals(0, count_private($this->linkDb));
@@ -553,7 +572,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(
             'File netscape_basic.htm (482 bytes) was successfully processed:'
             .' 2 links imported, 0 links overwritten, 0 links skipped.',
-            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf)
+            NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
         );
         $this->assertEquals(2, count($this->linkDb));
         $this->assertEquals(0, count_private($this->linkDb));
@@ -578,7 +597,7 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(
             'File same_date.htm (453 bytes) was successfully processed:'
             .' 3 links imported, 0 links overwritten, 0 links skipped.',
-            NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf)
+            NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf, $this->history)
         );
         $this->assertEquals(3, count($this->linkDb));
         $this->assertEquals(0, count_private($this->linkDb));
@@ -595,4 +614,32 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
             $this->linkDb[2]['id']
         );
     }
+
+    public function testImportCreateUpdateHistory()
+    {
+        $post = [
+            'privacy' => 'public',
+            'overwrite' => 'true',
+        ];
+        $files = file2array('netscape_basic.htm');
+        $nbLinks = 2;
+        NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history);
+        $history = $this->history->getHistory();
+        $this->assertEquals($nbLinks, count($history));
+        foreach ($history as $value) {
+            $this->assertEquals(History::CREATED, $value['event']);
+            $this->assertTrue(new DateTime('-5 seconds') < DateTime::createFromFormat(DateTime::ATOM, $value['datetime']));
+            $this->assertTrue(is_int($value['id']));
+        }
+
+        // re-import as private, enable overwriting
+        NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history);
+        $history = $this->history->getHistory();
+        $this->assertEquals($nbLinks * 2, count($history));
+        for ($i = 0 ; $i < $nbLinks ; $i++) {
+            $this->assertEquals(History::UPDATED, $history[$i]['event']);
+            $this->assertTrue(new DateTime('-5 seconds') < DateTime::createFromFormat(DateTime::ATOM, $history[$i]['datetime']));
+            $this->assertTrue(is_int($history[$i]['id']));
+        }
+    }
 }
index 2976d11612765dde427242073320c4d7f32bdb97..127fdc192ccab36ec57427ba270f444f9108b29f 100644 (file)
@@ -10,25 +10,46 @@ require_once 'application/TimeZone.php';
  */
 class TimeZoneTest extends PHPUnit_Framework_TestCase
 {
+    /**
+     * @var array of timezones
+     */
+    protected $installedTimezones;
+
+    public function setUp()
+    {
+        $this->installedTimezones = [
+            'Antarctica/Syowa',
+            'Europe/London',
+            'Europe/Paris',
+            'UTC'
+        ];
+    }
+
     /**
      * Generate a timezone selection form
      */
     public function testGenerateTimeZoneForm()
     {
-        $generated = generateTimeZoneForm();
+        $expected = [
+            'continents' => [
+                'Antarctica',
+                'Europe',
+                'UTC',
+                'selected' => '',
+            ],
+            'cities' => [
+                ['continent' => 'Antarctica', 'city' => 'Syowa'],
+                ['continent' => 'Europe',     'city' => 'London'],
+                ['continent' => 'Europe',     'city' => 'Paris'],
+                ['continent' => 'UTC',        'city' => 'UTC'],
+                'selected'    => '',
+            ]
+        ];
 
-        // HTML form
-        $this->assertStringStartsWith('Continent:<select', $generated[0]);
-        $this->assertContains('selected="selected"', $generated[0]);
-        $this->assertStringEndsWith('</select><br />', $generated[0]);
+        list($continents, $cities) = generateTimeZoneData($this->installedTimezones);
 
-        // Javascript handler
-        $this->assertStringStartsWith('<script>', $generated[1]);
-        $this->assertContains(
-            '<option value=\"Bermuda\">Bermuda<\/option>',
-            $generated[1]
-        );
-        $this->assertStringEndsWith('</script>', $generated[1]);
+        $this->assertEquals($expected['continents'], $continents);
+        $this->assertEquals($expected['cities'], $cities);
     }
 
     /**
@@ -36,28 +57,26 @@ class TimeZoneTest extends PHPUnit_Framework_TestCase
      */
     public function testGenerateTimeZoneFormPreselected()
     {
-        $generated = generateTimeZoneForm('Antarctica/Syowa');
-
-        // HTML form
-        $this->assertStringStartsWith('Continent:<select', $generated[0]);
-        $this->assertContains(
-            'value="Antarctica" selected="selected"',
-            $generated[0]
-        );
-        $this->assertContains(
-            'value="Syowa" selected="selected"',
-            $generated[0]
-        );
-        $this->assertStringEndsWith('</select><br />', $generated[0]);
+        $expected = [
+            'continents' => [
+                'Antarctica',
+                'Europe',
+                'UTC',
+                'selected' => 'Antarctica',
+            ],
+            'cities' => [
+                ['continent' => 'Antarctica', 'city' => 'Syowa'],
+                ['continent' => 'Europe',     'city' => 'London'],
+                ['continent' => 'Europe',     'city' => 'Paris'],
+                ['continent' => 'UTC',        'city' => 'UTC'],
+                'selected'   => 'Syowa',
+            ]
+        ];
 
+        list($continents, $cities) = generateTimeZoneData($this->installedTimezones, 'Antarctica/Syowa');
 
-        // Javascript handler
-        $this->assertStringStartsWith('<script>', $generated[1]);
-        $this->assertContains(
-            '<option value=\"Bermuda\">Bermuda<\/option>',
-            $generated[1]
-        );
-        $this->assertStringEndsWith('</script>', $generated[1]);
+        $this->assertEquals($expected['continents'], $continents);
+        $this->assertEquals($expected['cities'], $cities);
     }
 
     /**
index e70cc1aef10197d66718149a0ded03f85cbec072..d6a0aad5e8e0201da8441dcaa83eab6d7181575b 100644 (file)
@@ -4,6 +4,7 @@
  */
 
 require_once 'application/Utils.php';
+require_once 'application/Languages.php';
 require_once 'tests/utils/ReferenceSessionIdHashes.php';
 
 // Initialize reference data before PHPUnit starts a session
@@ -326,4 +327,94 @@ class UtilsTest extends PHPUnit_Framework_TestCase
         $this->assertFalse(format_date([]));
         $this->assertFalse(format_date(null));
     }
+
+    /**
+     * Test is_integer_mixed with valid values
+     */
+    public function testIsIntegerMixedValid()
+    {
+        $this->assertTrue(is_integer_mixed(12));
+        $this->assertTrue(is_integer_mixed('12'));
+        $this->assertTrue(is_integer_mixed(-12));
+        $this->assertTrue(is_integer_mixed('-12'));
+        $this->assertTrue(is_integer_mixed(0));
+        $this->assertTrue(is_integer_mixed('0'));
+        $this->assertTrue(is_integer_mixed(0x0a));
+    }
+
+    /**
+     * Test is_integer_mixed with invalid values
+     */
+    public function testIsIntegerMixedInvalid()
+    {
+        $this->assertFalse(is_integer_mixed(true));
+        $this->assertFalse(is_integer_mixed(false));
+        $this->assertFalse(is_integer_mixed([]));
+        $this->assertFalse(is_integer_mixed(['test']));
+        $this->assertFalse(is_integer_mixed([12]));
+        $this->assertFalse(is_integer_mixed(new DateTime()));
+        $this->assertFalse(is_integer_mixed('0x0a'));
+        $this->assertFalse(is_integer_mixed('12k'));
+        $this->assertFalse(is_integer_mixed('k12'));
+        $this->assertFalse(is_integer_mixed(''));
+    }
+
+    /**
+     * Test return_bytes
+     */
+    public function testReturnBytes()
+    {
+        $this->assertEquals(2 * 1024, return_bytes('2k'));
+        $this->assertEquals(2 * 1024, return_bytes('2K'));
+        $this->assertEquals(2 * (pow(1024, 2)), return_bytes('2m'));
+        $this->assertEquals(2 * (pow(1024, 2)), return_bytes('2M'));
+        $this->assertEquals(2 * (pow(1024, 3)), return_bytes('2g'));
+        $this->assertEquals(2 * (pow(1024, 3)), return_bytes('2G'));
+        $this->assertEquals(374, return_bytes('374'));
+        $this->assertEquals(374, return_bytes(374));
+        $this->assertEquals(0, return_bytes('0'));
+        $this->assertEquals(0, return_bytes(0));
+        $this->assertEquals(-1, return_bytes('-1'));
+        $this->assertEquals(-1, return_bytes(-1));
+        $this->assertEquals('', return_bytes(''));
+    }
+
+    /**
+     * Test human_bytes
+     */
+    public function testHumanBytes()
+    {
+        $this->assertEquals('2kiB', human_bytes(2 * 1024));
+        $this->assertEquals('2kiB', human_bytes(strval(2 * 1024)));
+        $this->assertEquals('2MiB', human_bytes(2 * (pow(1024, 2))));
+        $this->assertEquals('2MiB', human_bytes(strval(2 * (pow(1024, 2)))));
+        $this->assertEquals('2GiB', human_bytes(2 * (pow(1024, 3))));
+        $this->assertEquals('2GiB', human_bytes(strval(2 * (pow(1024, 3)))));
+        $this->assertEquals('374B', human_bytes(374));
+        $this->assertEquals('374B', human_bytes('374'));
+        $this->assertEquals('232kiB', human_bytes(237481));
+        $this->assertEquals('Unlimited', human_bytes('0'));
+        $this->assertEquals('Unlimited', human_bytes(0));
+        $this->assertEquals('Setting not set', human_bytes(''));
+    }
+
+    /**
+     * Test get_max_upload_size with formatting
+     */
+    public function testGetMaxUploadSize()
+    {
+        $this->assertEquals('1MiB', get_max_upload_size(2097152, '1024k'));
+        $this->assertEquals('1MiB', get_max_upload_size('1m', '2m'));
+        $this->assertEquals('100B', get_max_upload_size(100, 100));
+    }
+
+    /**
+     * Test get_max_upload_size without formatting
+     */
+    public function testGetMaxUploadSizeRaw()
+    {
+        $this->assertEquals('1048576', get_max_upload_size(2097152, '1024k', false));
+        $this->assertEquals('1048576', get_max_upload_size('1m', '2m', false));
+        $this->assertEquals('100', get_max_upload_size(100, 100, false));
+    }
 }
diff --git a/tests/plugins/PluginReadityourselfTest.php b/tests/plugins/PluginReadityourselfTest.php
deleted file mode 100644 (file)
index bbba967..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-<?php
-use Shaarli\Config\ConfigManager;
-
-/**
- * PluginReadityourselfTest.php.php
- */
-
-require_once 'plugins/readityourself/readityourself.php';
-
-/**
- * Class PluginWallabagTest
- * Unit test for the Wallabag plugin
- */
-class PluginReadityourselfTest extends PHPUnit_Framework_TestCase
-{
-    /**
-     * Reset plugin path
-     */
-    public function setUp()
-    {
-        PluginManager::$PLUGINS_PATH = 'plugins';
-    }
-
-    /**
-     * Test Readityourself init without errors.
-     */
-    public function testReadityourselfInitNoError()
-    {
-        $conf = new ConfigManager('');
-        $conf->set('plugins.READITYOUSELF_URL', 'value');
-        $errors = readityourself_init($conf);
-        $this->assertEmpty($errors);
-    }
-
-    /**
-     * Test Readityourself init with errors.
-     */
-    public function testReadityourselfInitError()
-    {
-        $conf = new ConfigManager('');
-        $errors = readityourself_init($conf);
-        $this->assertNotEmpty($errors);
-    }
-
-    /**
-     * Test render_linklist hook.
-     */
-    public function testReadityourselfLinklist()
-    {
-        $conf = new ConfigManager('');
-        $conf->set('plugins.READITYOUSELF_URL', 'value');
-        $str = 'http://randomstr.com/test';
-        $data = array(
-            'title' => $str,
-            'links' => array(
-                array(
-                    'url' => $str,
-                )
-            )
-        );
-
-        $data = hook_readityourself_render_linklist($data, $conf);
-        $link = $data['links'][0];
-        // data shouldn't be altered
-        $this->assertEquals($str, $data['title']);
-        $this->assertEquals($str, $link['url']);
-
-        // plugin data
-        $this->assertEquals(1, count($link['link_plugin']));
-        $this->assertNotFalse(strpos($link['link_plugin'][0], $str));
-    }
-
-    /**
-     * Test without config: nothing should happened.
-     */
-    public function testReadityourselfLinklistWithoutConfig()
-    {
-        $conf = new ConfigManager('');
-        $conf->set('plugins.READITYOUSELF_URL', null);
-        $str = 'http://randomstr.com/test';
-        $data = array(
-            'title' => $str,
-            'links' => array(
-                array(
-                    'url' => $str,
-                )
-            )
-        );
-
-        $data = hook_readityourself_render_linklist($data, $conf);
-        $link = $data['links'][0];
-        // data shouldn't be altered
-        $this->assertEquals($str, $data['title']);
-        $this->assertEquals($str, $link['url']);
-
-        // plugin data
-        $this->assertArrayNotHasKey('link_plugin', $link);
-    }
-}
index 1226148743a038f6c02835644538aa9c08db2ead..76a1b9fd506f9e6bf17cb0cd520d5a9694da9bac 100644 (file)
@@ -34,7 +34,7 @@
         <div class="pure-u-lg-{$ratioLabel} pure-u-1">
           <div class="form-label">
             <label for="titleLink">
-              <span class="label-name">{'Title link'|t}</span><br>
+              <span class="label-name">{'Home link'|t}</span><br>
               <span class="label-desc">{'Default value'|t}: ?</span>
             </label>
           </div>
         <div class="pure-u-lg-{$ratioLabel} pure-u-1 ">
           <div class="form-label">
             <label>
-              <span class="label-name">{'Timezone'|t}</span>
+              <span class="label-name">{'Timezone'|t}</span><br>
+              <span class="label-desc">{'Continent'|t} &middot; {'City'|t}</span>
             </label>
           </div>
         </div>
         <div class="pure-u-lg-{$ratioInput} pure-u-1 ">
           <div class="form-input">
-            {ignore}FIXME! too hackish, needs to be fixed upstream{/ignore}
-            <div class="timezone" id="timezone-remove">{$timezone_form}</div>
-            <div class="timezone" id="timezone-add"></div>
+            <div class="timezone">
+              <select id="continent" name="continent">
+                {loop="$continents"}
+                  {if="$key !== 'selected'"}
+                    <option value="{$value}" {if="$continents.selected === $value"}selected{/if}>
+                      {$value}
+                    </option>
+                  {/if}
+                {/loop}
+              </select>
+              <select id="city" name="city">
+                {loop="$cities"}
+                  {if="$key !== 'selected'"}
+                    <option value="{$value.city}"
+                            {if="$cities.selected === $value.city"}selected{/if}
+                            data-continent="{$value.continent}">
+                      {$value.city}
+                    </option>
+                  {/if}
+                {/loop}
+              </select>
+            </div>
           </div>
         </div>
       </div>
index e6e521e8fc3d0280e2ec04f622c88a5792271968..1f040685caa5195f0a2e9a41ec199bf720bee178 100644 (file)
@@ -18,6 +18,7 @@
       <div class="center" id="import-field">
         <input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}">
         <input type="file" name="filetoupload">
+        <p><br>Maximum size allowed: <strong>{$maxfilesizeHuman}</strong></p>
       </div>
 
       <div class="pure-g">
index 663397ac51f896008b33ecffaafb7f82592db22f..164d453b1d6c162bfa6b5a28b9203dbdfdb10d3e 100644 (file)
       </div>
       <div class="pure-u-lg-{$ratioInput} pure-u-1">
         <div class="form-input">
-          <input type="text" name="setpassword" id="password">
+          <input type="password" name="setpassword" id="password">
         </div>
       </div>
     </div>
 
     <div class="pure-g">
-      <div class="pure-u-lg-{$ratioLabel} pure-u-1 ">
+      <div class="pure-u-lg-{$ratioLabel} pure-u-1">
         <div class="form-label">
-          <label>
-            <span class="label-name">{'Timezone'|t}</span>
+          <label for="title">
+            <span class="label-name">{'Shaarli title'|t}</span>
           </label>
         </div>
       </div>
-      <div class="pure-u-lg-{$ratioInput} pure-u-1 ">
+      <div class="pure-u-lg-{$ratioInput} pure-u-1">
         <div class="form-input">
-          {ignore}FIXME! too hackish, needs to be fixed upstream{/ignore}
-          <div class="timezone" id="timezone-remove">{$timezone_html}</div>
-          <div class="timezone" id="timezone-add"></div>
+          <input type="text" name="title" id="title" placeholder="{'My links'|t}">
         </div>
       </div>
     </div>
     <div class="pure-g">
       <div class="pure-u-lg-{$ratioLabel} pure-u-1">
         <div class="form-label">
-          <label for="title">
-            <span class="label-name">{'Shaarli title'|t}</span>
+          <label>
+            <span class="label-name">{'Timezone'|t}</span><br>
+            <span class="label-desc">{'Continent'|t} &middot; {'City'|t}</span>
           </label>
         </div>
       </div>
       <div class="pure-u-lg-{$ratioInput} pure-u-1">
         <div class="form-input">
-          <input type="text" name="title" id="title" placeholder="{'My links'|t}">
+          <div class="timezone">
+            <select id="continent" name="continent">
+              {loop="$continents"}
+                {if="$key !== 'selected'"}
+                  <option value="{$value}" {if="$continents.selected === $value"}selected{/if}>
+                    {$value}
+                  </option>
+                {/if}
+              {/loop}
+            </select>
+            <select id="city" name="city">
+              {loop="$cities"}
+                {if="$key !== 'selected'"}
+                  <option value="{$value.city}"
+                          {if="$cities.selected === $value.city"}selected{/if}
+                          data-continent="{$value.continent}">
+                    {$value.city}
+                  </option>
+                {/if}
+              {/loop}
+            </select>
+          </div>
         </div>
       </div>
     </div>
index edcf280944a6f723551bf5df011f9228f71588f5..4d47fcd0c2cd4aaf3be8080e84c61782adabd247 100644 (file)
@@ -76,9 +76,12 @@ window.onload = function () {
             }
         }
 
-        document.getElementById('menu-toggle').addEventListener('click', function (e) {
-            toggleMenu();
-        });
+        var menuToggle = document.getElementById('menu-toggle');
+        if (menuToggle != null) {
+            menuToggle.addEventListener('click', function (e) {
+                toggleMenu();
+            });
+        }
 
         window.addEventListener(WINDOW_CHANGE_EVENT, closeMenu);
     })(this, this.document);
@@ -298,21 +301,6 @@ window.onload = function () {
         });
     }
 
-    /**
-     * TimeZome select
-     * FIXME! way too hackish
-     */
-    var toRemove = document.getElementById('timezone-remove');
-    if (toRemove != null) {
-        var firstSelect = toRemove.getElementsByTagName('select')[0];
-        var secondSelect = toRemove.getElementsByTagName('select')[1];
-        toRemove.parentNode.removeChild(toRemove);
-        var toAdd = document.getElementById('timezone-add');
-        var newTimezone = '<span class="timezone-continent">Continent ' + firstSelect.outerHTML + '</span>';
-        newTimezone += ' <span class="timezone-country">Country ' + secondSelect.outerHTML + '</span>';
-        toAdd.innerHTML = newTimezone;
-    }
-
     /**
      * Awesomplete trigger.
      */
@@ -365,6 +353,15 @@ window.onload = function () {
             }
         });
     });
+
+    var continent = document.getElementById('continent');
+    var city = document.getElementById('city');
+    if (continent != null && city != null) {
+        continent.addEventListener('change', function(event) {
+            hideTimezoneCities(city, continent.options[continent.selectedIndex].value, true);
+        });
+        hideTimezoneCities(city, continent.options[continent.selectedIndex].value, false);
+    }
 };
 
 function activateFirefoxSocial(node) {
@@ -390,3 +387,25 @@ function activateFirefoxSocial(node) {
     var activate = new CustomEvent("ActivateSocialFeature");
     node.dispatchEvent(activate);
 }
+
+/**
+ * Add the class 'hidden' to city options not attached to the current selected continent.
+ *
+ * @param cities           List of <option> elements
+ * @param currentContinent Current selected continent
+ * @param reset            Set to true to reset the selected value
+ */
+function hideTimezoneCities(cities, currentContinent, reset = false) {
+    var first = true;
+    [].forEach.call(cities, function(option) {
+        if (option.getAttribute('data-continent') != currentContinent) {
+            option.className = 'hidden';
+        } else {
+            option.className = '';
+            if (reset === true && first === true) {
+                option.setAttribute('selected', 'selected');
+                first = false;
+            }
+        }
+    });
+}
index 704389c596103954ea1ae532b10fd6db5ecaf3bf..479284eb1aa2ec993ceb02caf5281093871cb0a4 100644 (file)
@@ -4,7 +4,6 @@
 <body onload="document.configform.title.focus();">
 <div id="pageheader">
   {include="page.header"}
-  {$timezone_js}
   <form method="POST" action="#" name="configform" id="configform">
     <input type="hidden" name="token" value="{$token}">
     <table id="configuration_table">
@@ -15,7 +14,7 @@
       </tr>
 
       <tr>
-        <td><b>Title link:</b></td>
+        <td><b>Home link:</b></td>
         <td><input type="text" name="titleLink" id="titleLink" size="50" value="{$titleLink}"><br/><label
             for="titleLink">(default value is: ?)</label></td>
       </tr>
 
       <tr>
         <td><b>Timezone:</b></td>
-        <td>{$timezone_form}</td>
+        <td>
+          <select id="continent" name="continent">
+            {loop="$continents"}
+              {if="$key !== 'selected'"}
+                <option value="{$value}" {if="$continents.selected === $value"}selected{/if}>
+                  {$value}
+                </option>
+              {/if}
+            {/loop}
+          </select>
+          <select id="city" name="city">
+            {loop="$cities"}
+              {if="$key !== 'selected'"}
+                <option value="{$value.city}"
+                        {if="$cities.selected === $value.city"}selected{/if}
+                        data-continent="{$value.continent}">
+                  {$value.city}
+                </option>
+              {/if}
+            {/loop}
+          </select>
+        </td>
       </tr>
 
       <tr>
index 7ca567e7549f5b3c729673a38c8e8455bfd6ddf9..9c72d9938bca0dd6750daf950c778b5c0cb8ae6d 100644 (file)
@@ -41,6 +41,10 @@ strong {
     font-weight: bold;
 }
 
+.hidden {
+    display: none;
+}
+
 /* Buttons */
 .bigbutton, #pageheader a.bigbutton  {
     background-color: #c0c0c0;
index 071e1160e5bbae106b58206bdd5c152f6891b45f..bb9e4a562040b410b00e90bbc6a6a63b5520d9ab 100644 (file)
@@ -5,7 +5,7 @@
 <div id="pageheader">
   {include="page.header"}
   <div id="uploaddiv">
-    Import Netscape HTML bookmarks (as exported from Firefox/Chrome/Opera/Delicious/Diigo...) (Max: {$maxfilesize} bytes).
+    Import Netscape HTML bookmarks (as exported from Firefox/Chrome/Opera/Delicious/Diigo...) (Max: {$maxfilesize}).
     <form method="POST" action="?do=import" enctype="multipart/form-data"
           name="uploadform" id="uploadform">
       <input type="hidden" name="token" value="{$token}">
index 42874dcdb42c61963050599c6df922a079b615f9..aca890d6c3a549fca924fa02a58a5fa384a7dae6 100644 (file)
@@ -1,6 +1,6 @@
 <!DOCTYPE html>
 <html>
-<head>{include="includes"}{$timezone_js}</head>
+<head>{include="includes"}</head>
 <body onload="document.installform.setlogin.focus();">
 <div id="install">
     <h1>Shaarli</h1>
@@ -9,7 +9,31 @@
         <table>
             <tr><td><b>Login:</b></td><td><input type="text" name="setlogin" size="30"></td></tr>
             <tr><td><b>Password:</b></td><td><input type="password" name="setpassword" size="30"></td></tr>
-            {$timezone_html}
+            <tr>
+                <td><b>Timezone:</b></td>
+                <td>
+                    <select id="continent" name="continent">
+                        {loop="$continents"}
+                        {if="$key !== 'selected'"}
+                        <option value="{$value}" {if="$continents.selected === $value"}selected{/if}>
+                        {$value}
+                        </option>
+                        {/if}
+                        {/loop}
+                    </select>
+                    <select id="city" name="city">
+                        {loop="$cities"}
+                        {if="$key !== 'selected'"}
+                        <option value="{$value.city}"
+                                {if="$cities.selected === $value.city"}selected{/if}
+                        data-continent="{$value.continent}">
+                        {$value.city}
+                        </option>
+                        {/if}
+                        {/loop}
+                    </select>
+                </td>
+            </tr>
             <tr><td><b>Page title:</b></td><td><input type="text" name="title" size="30"></td></tr>
             <tr><td valign="top"><b>Update:</b></td><td>
                 <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 (file)
index 0000000..9bcc96f
--- /dev/null
@@ -0,0 +1,32 @@
+window.onload = function () {
+    var continent = document.getElementById('continent');
+    var city = document.getElementById('city');
+    if (continent != null && city != null) {
+        continent.addEventListener('change', function(event) {
+            hideTimezoneCities(city, continent.options[continent.selectedIndex].value, true);
+        });
+        hideTimezoneCities(city, continent.options[continent.selectedIndex].value, false);
+    }
+};
+
+/**
+ * Add the class 'hidden' to city options not attached to the current selected continent.
+ *
+ * @param cities           List of <option> elements
+ * @param currentContinent Current selected continent
+ * @param reset            Set to true to reset the selected value
+ */
+function hideTimezoneCities(cities, currentContinent, reset = false) {
+    var first = true;
+    [].forEach.call(cities, function(option) {
+        if (option.getAttribute('data-continent') != currentContinent) {
+            option.className = 'hidden';
+        } else {
+            option.className = '';
+            if (reset === true && first === true) {
+                option.setAttribute('selected', 'selected');
+                first = false;
+            }
+        }
+    });
+}
index 006d1d683b465d303ae56c4332d659bedab0533b..4ce0803a064dcbf3a78efed3043566b7986cf1ca 100644 (file)
@@ -26,6 +26,7 @@
 <script>function confirmDeleteLink() { var agree=confirm("Are you sure you want to delete this link ?"); if (agree) return true ; else return false ; }</script>
 {/if}
 
+<script src="js/shaarli.js"></script>
 {loop="$plugins_footer.js_files"}
        <script src="{$value}#"></script>
 {/loop}
index cce61ec464b8e54c8e1897a986634b5044237b7d..8a58844ee5556472db98089393eeef948f82c760 100644 (file)
@@ -1,5 +1,5 @@
 
-<div id="logo" title="Share your links !" onclick="document.location='?';"></div>
+<div id="logo" title="Share your links !" onclick="document.location='{$titleLink}';"></div>
 
 <div id="linkcount" class="nomobile">
     {if="!empty($linkcount)"}{$linkcount} links{/if}<br>
@@ -16,7 +16,7 @@
 {if="!empty($_GET['source']) && $_GET['source']=='bookmarklet'"}
     {ignore} When called as a popup from bookmarklet, do not display menu. {/ignore}
 {else}
-<li><a href="?" class="nomobile">Home</a></li>
+<li><a href="{$titleLink}" class="nomobile">Home</a></li>
     {if="isLoggedIn()"}
     <li><a href="?do=logout">Logout</a></li>
     <li><a href="?do=tools">Tools</a></li>