From b2306b0c783365e3f8110ae25bc93f2630b8b2c8 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 16 Jan 2017 12:30:18 +0100 Subject: Move database read/write to FileUtils class + additional unit tests --- application/FileUtils.php | 75 ++++++++++++++++++++--- application/LinkDB.php | 31 +--------- application/exceptions/IOException.php | 22 +++++++ tests/FileUtilsTest.php | 108 +++++++++++++++++++++++++++++++++ tests/LinkDBTest.php | 2 +- 5 files changed, 198 insertions(+), 40 deletions(-) create mode 100644 application/exceptions/IOException.php create mode 100644 tests/FileUtilsTest.php 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/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 @@ 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/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 @@ 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() { -- cgit v1.2.3 From 4306b184c4471825f916d895b047ed03fdf58985 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 16 Jan 2017 12:31:08 +0100 Subject: History mechanism Use case: rest API service * saved by default in data/history * same format as datastore.php * traced events: * save/edit/delete link * change settings or plugins settings * rename tag --- application/History.php | 183 +++++++++++++++++++ application/NetscapeBookmarkUtils.php | 5 +- application/config/ConfigManager.php | 1 + index.php | 23 ++- tests/HistoryTest.php | 195 +++++++++++++++++++++ tests/NetscapeBookmarkUtils/BookmarkImportTest.php | 81 +++++++-- 6 files changed, 469 insertions(+), 19 deletions(-) create mode 100644 application/History.php create mode 100644 tests/HistoryTest.php diff --git a/application/History.php b/application/History.php new file mode 100644 index 00000000..c06067df --- /dev/null +++ b/application/History.php @@ -0,0 +1,183 @@ +historyFilePath = $historyFilePath; + if ($retentionTime !== null) { + $this->retentionTime = $retentionTime; + } + $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) + { + $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() + { + return $this->history; + } +} 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/config/ConfigManager.php b/application/config/ConfigManager.php index 7bfbfc72..86a917fb 100644 --- a/application/config/ConfigManager.php +++ b/application/config/ConfigManager.php @@ -301,6 +301,7 @@ class ConfigManager $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/index.php b/index.php index cc7f3ca3..7f357c69 100644 --- a/index.php +++ b/index.php @@ -65,6 +65,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'; @@ -754,6 +755,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)); @@ -1146,6 +1153,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) { @@ -1177,6 +1185,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; } @@ -1206,6 +1215,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 ''; @@ -1223,6 +1233,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 ''; @@ -1257,11 +1268,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. @@ -1300,6 +1313,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')) { @@ -1346,6 +1364,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 ''; exit; } @@ -1528,7 +1547,8 @@ function renderPage($conf, $pluginManager, $LINKSDB) $_POST, $_FILES, $LINKSDB, - $conf + $conf, + $history ); echo ''; @@ -1557,6 +1577,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']); diff --git a/tests/HistoryTest.php b/tests/HistoryTest.php new file mode 100644 index 00000000..79322249 --- /dev/null +++ b/tests/HistoryTest.php @@ -0,0 +1,195 @@ +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); + new History(self::$historyFilePath); + } + + /** + * 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'); + // gzinflate generates a warning + @new History(self::$historyFilePath); + } + + /** + * 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']); + } +} diff --git a/tests/NetscapeBookmarkUtils/BookmarkImportTest.php b/tests/NetscapeBookmarkUtils/BookmarkImportTest.php index 5925a8e1..f838f259 100644 --- a/tests/NetscapeBookmarkUtils/BookmarkImportTest.php +++ b/tests/NetscapeBookmarkUtils/BookmarkImportTest.php @@ -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'])); + } + } } -- cgit v1.2.3 From d16ca2e22f3d7325fc9593fccd8523eebe226567 Mon Sep 17 00:00:00 2001 From: ArthurHoaro Date: Mon, 16 Jan 2017 12:50:36 +0100 Subject: History: lazy loading for the history file Only read it when it's necessary --- application/History.php | 17 +++++++++++++++++ tests/HistoryTest.php | 18 +++++++++++++++--- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/application/History.php b/application/History.php index c06067df..f93b0356 100644 --- a/application/History.php +++ b/application/History.php @@ -70,6 +70,15 @@ class History 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(); } @@ -120,6 +129,10 @@ class History */ protected function addEvent($status, $id = null) { + if ($this->history === null) { + $this->initialize(); + } + $item = [ 'event' => $status, 'datetime' => (new DateTime())->format(DateTime::ATOM), @@ -178,6 +191,10 @@ class History */ public function getHistory() { + if ($this->history === null) { + $this->initialize(); + } + return $this->history; } } diff --git a/tests/HistoryTest.php b/tests/HistoryTest.php index 79322249..91525845 100644 --- a/tests/HistoryTest.php +++ b/tests/HistoryTest.php @@ -21,9 +21,19 @@ class HistoryTest extends PHPUnit_Framework_TestCase /** * Test that the history file is created if it doesn't exist. */ - public function testConstructFileCreated() + 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); } @@ -37,7 +47,8 @@ class HistoryTest extends PHPUnit_Framework_TestCase { touch(self::$historyFilePath); chmod(self::$historyFilePath, 0440); - new History(self::$historyFilePath); + $history = new History(self::$historyFilePath); + $history->updateSettings(); } /** @@ -49,8 +60,9 @@ class HistoryTest extends PHPUnit_Framework_TestCase public function testConstructNotParsable() { file_put_contents(self::$historyFilePath, 'not parsable'); + $history = new History(self::$historyFilePath); // gzinflate generates a warning - @new History(self::$historyFilePath); + @$history->updateSettings(); } /** -- cgit v1.2.3