Merge pull request #764 from ArthurHoaro/feature/history
History mechanism
This commit is contained in:
commit
f9ff7f1b69
11 changed files with 697 additions and 60 deletions
|
@ -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 /* ';
|
||||
|
||||
/**
|
||||
* Construct a new IOException
|
||||
*
|
||||
* @param string $path path to the resource that cannot be accessed
|
||||
* @param string $message Custom exception message.
|
||||
* @var string
|
||||
*/
|
||||
public function __construct($path, $message = '')
|
||||
protected static $phpSuffix = ' */ ?>';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
|
200
application/History.php
Normal file
200
application/History.php
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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...)
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -95,10 +95,11 @@ private static function importStatus(
|
|||
* @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 @@ public static function import($post, $files, $linkDb, $conf)
|
|||
$linkDb[$existingLink['id']] = $newLink;
|
||||
$importCount++;
|
||||
$overwriteCount++;
|
||||
$history->updateLink($newLink);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -193,6 +195,7 @@ public static function import($post, $files, $linkDb, $conf)
|
|||
$newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']);
|
||||
$linkDb[$newLink['id']] = $newLink;
|
||||
$importCount++;
|
||||
$history->addLink($newLink);
|
||||
}
|
||||
|
||||
$linkDb->save($conf->get('resource.page_cache'));
|
||||
|
|
|
@ -301,6 +301,7 @@ protected function setDefaultValues()
|
|||
$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/');
|
||||
|
|
22
application/exceptions/IOException.php
Normal file
22
application/exceptions/IOException.php
Normal file
|
@ -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 .'"';
|
||||
}
|
||||
}
|
23
index.php
23
index.php
|
@ -62,6 +62,7 @@
|
|||
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';
|
||||
|
@ -727,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));
|
||||
|
@ -1125,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) {
|
||||
|
@ -1159,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;
|
||||
}
|
||||
|
@ -1188,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>';
|
||||
|
@ -1205,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>';
|
||||
|
@ -1239,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.
|
||||
|
@ -1282,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')) {
|
||||
|
@ -1332,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; }
|
||||
|
@ -1529,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>';
|
||||
|
@ -1558,6 +1578,7 @@ function($a, $b) { return $a['order'] - $b['order']; }
|
|||
|
||||
// Plugin administration form action
|
||||
if ($targetPage == Router::$PAGE_SAVE_PLUGINSADMIN) {
|
||||
$history->updateSettings();
|
||||
try {
|
||||
if (isset($_POST['parameters_form'])) {
|
||||
unset($_POST['parameters_form']);
|
||||
|
|
108
tests/FileUtilsTest.php
Normal file
108
tests/FileUtilsTest.php
Normal file
|
@ -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']));
|
||||
}
|
||||
}
|
207
tests/HistoryTest.php
Normal file
207
tests/HistoryTest.php
Normal file
|
@ -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']);
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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 @@ protected function setUp()
|
|||
$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 @@ public function testImportEmptyData()
|
|||
$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 @@ public function testImportNoDoctype()
|
|||
$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 @@ public function testImportInternetExplorerEncoding()
|
|||
$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 @@ public function testImportNested()
|
|||
$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 @@ public function testImportDefaultPrivacyNoPost()
|
|||
$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 @@ public function testImportKeepPrivacy()
|
|||
$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 @@ public function testImportAsPublic()
|
|||
$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 @@ public function testImportAsPrivate()
|
|||
$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 @@ public function testOverwriteAsPublic()
|
|||
$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 @@ public function testOverwriteAsPublic()
|
|||
$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 @@ public function testOverwriteAsPrivate()
|
|||
$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 @@ public function testOverwriteAsPrivate()
|
|||
$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 @@ public function testSkipOverwrite()
|
|||
$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 @@ public function testSkipOverwrite()
|
|||
$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 @@ public function testSetDefaultTags()
|
|||
$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 @@ public function testSanitizeDefaultTags()
|
|||
$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 @@ public function testImportSameDate()
|
|||
$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 @@ public function testImportSameDate()
|
|||
$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']));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue