<?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;
}
}
--- /dev/null
+<?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;
+ }
+}
// 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...)
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) {
*/
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);
}
/**
* @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'];
$linkDb[$existingLink['id']] = $newLink;
$importCount++;
$overwriteCount++;
+ $history->updateLink($newLink);
continue;
}
$newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']);
$linkDb[$newLink['id']] = $newLink;
$importCount++;
+ $history->addLink($newLink);
}
$linkDb->save($conf->get('resource.page_cache'));
$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/');
--- /dev/null
+<?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 .'"';
+ }
+}
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';
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));
$conf->set('api.secret', escape($_POST['apiSecret']));
try {
$conf->write(isLoggedIn());
+ $history->updateSettings();
invalidateCaches($conf->get('resource.page_cache'));
}
catch(Exception $e) {
$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;
}
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>';
$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>';
$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.
$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')) {
$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; }
$_POST,
$_FILES,
$LINKSDB,
- $conf
+ $conf,
+ $history
);
echo '<script>alert("'.$status.'");document.location=\'?do='
.Router::$PAGE_IMPORT .'\';</script>';
// Plugin administration form action
if ($targetPage == Router::$PAGE_SAVE_PLUGINSADMIN) {
+ $history->updateSettings();
try {
if (isset($_POST['parameters_form'])) {
unset($_POST['parameters_form']);
--- /dev/null
+<?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']));
+ }
+}
--- /dev/null
+<?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']);
+ }
+}
* Attempt to instantiate a LinkDB whereas the datastore is not writable
*
* @expectedException IOException
- * @expectedExceptionMessageRegExp /Error accessing\nnull/
+ * @expectedExceptionMessageRegExp /Error accessing "null"/
*/
public function testConstructDatastoreNotWriteable()
{
*/
protected static $testDatastore = 'sandbox/datastore.php';
+ /**
+ * @var string History file path
+ */
+ protected static $historyFilePath = 'sandbox/history.php';
+
/**
* @var LinkDB private LinkDB instance
*/
*/
protected $conf;
+ /**
+ * @var History instance.
+ */
+ protected $history;
+
/**
* @var string Save the current timezone.
*/
$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()
$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));
}
$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));
}
$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));
$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));
$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));
$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));
$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));
$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));
$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));
$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));
$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));
$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));
$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));
$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));
$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));
$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));
$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));
$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']));
+ }
+ }
}