From: ArthurHoaro Date: Sun, 7 May 2017 14:02:14 +0000 (+0200) Subject: Merge pull request #856 from ArthurHoaro/api/delete-link X-Git-Tag: v0.9.0~8 X-Git-Url: https://git.immae.eu/?a=commitdiff_plain;h=b8fcb7d4403a344158ab5d2c8979bdd002e6001d;hp=0843848c1d18e92504c43d181063a2012f8fd5b9;p=github%2Fshaarli%2FShaarli.git Merge pull request #856 from ArthurHoaro/api/delete-link API: add DELETE endpoint --- 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 @@ '; /** - * 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 index 00000000..f93b0356 --- /dev/null +++ b/application/History.php @@ -0,0 +1,200 @@ +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; + } +} 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 // Link date storage format const LINK_DATE_FORMAT = 'Ymd_His'; - // Datastore PHP prefix - protected static $phpPrefix = ''; - // 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); } /** 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 * @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')); 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 @@ '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] .= '