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 @@ '; + + /** + * 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. + * + * @throws IOException The destination file can't be written. + */ + 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/LinkDB.php b/application/LinkDB.php index 4cee2af9..2fb15040 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...) @@ -295,16 +289,7 @@ private function read() 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 @@ private function read() */ 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/exceptions/IOException.php b/application/exceptions/IOException.php new file mode 100644 index 00000000..b563b23d --- /dev/null +++ b/application/exceptions/IOException.php @@ -0,0 +1,22 @@ +path = $path; + $this->message = empty($message) ? 'Error accessing' : $message; + $this->message .= ' "' . $this->path .'"'; + } +} diff --git a/tests/FileUtilsTest.php b/tests/FileUtilsTest.php new file mode 100644 index 00000000..d764e495 --- /dev/null +++ b/tests/FileUtilsTest.php @@ -0,0 +1,108 @@ +assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0); + $this->assertTrue(startsWith(file_get_contents(self::$file), '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/LinkDBTest.php b/tests/LinkDBTest.php index 1f62a34a..7bf98f92 100644 --- a/tests/LinkDBTest.php +++ b/tests/LinkDBTest.php @@ -101,7 +101,7 @@ public function testConstructLoggedOut() * Attempt to instantiate a LinkDB whereas the datastore is not writable * * @expectedException IOException - * @expectedExceptionMessageRegExp /Error accessing\nnull/ + * @expectedExceptionMessageRegExp /Error accessing "null"/ */ public function testConstructDatastoreNotWriteable() {