diff --git a/library/Icinga/Application/Web.php b/library/Icinga/Application/Web.php index fca67901f..cd7a31aa6 100644 --- a/library/Icinga/Application/Web.php +++ b/library/Icinga/Application/Web.php @@ -217,7 +217,7 @@ class Web extends ApplicationBootstrap if ($authenticationManager->isAuthenticated() === true) { $user = $authenticationManager->getUser(); - $preferences = new Preferences(); + $preferences = new Preferences(array()); $user->setPreferences($preferences); $this->request->setUser($user); diff --git a/library/Icinga/User/Preferences.php b/library/Icinga/User/Preferences.php index 7151e8cf4..030a4571e 100644 --- a/library/Icinga/User/Preferences.php +++ b/library/Icinga/User/Preferences.php @@ -28,9 +28,203 @@ namespace Icinga\User; +use \SplObjectStorage; +use \SplObserver; +use \SplSubject; +use Icinga\User\Preferences\ChangeSet; +use Icinga\Exception\ProgrammingError; + /** * Handling retrieve and persist of user preferences */ -class Preferences +class Preferences implements SplSubject, \Countable { + /** + * Container for all preferences + * + * @var array + */ + private $preferences = array(); + + /** + * All observers for changes + * + * @var SplObserver[] + */ + private $observer = array(); + + /** + * Current change set + * + * @var ChangeSet + */ + private $changeSet; + + /** + * Flag how commits are handled + * + * @var bool + */ + private $autoCommit = true; + + /** + * Create a new instance + * @param array $initialPreferences + */ + public function __construct(array $initialPreferences) + { + $this->preferences = $initialPreferences; + $this->changeSet = new ChangeSet(); + } + + /** + * (PHP 5 >= 5.1.0)
+ * Attach an SplObserver + * @link http://php.net/manual/en/splsubject.attach.php + * @param SplObserver $observer

+ * The SplObserver to attach. + *

+ * @return void + */ + public function attach(SplObserver $observer) + { + $this->observer[] = $observer; + } + + /** + * (PHP 5 >= 5.1.0)
+ * Detach an observer + * @link http://php.net/manual/en/splsubject.detach.php + * @param SplObserver $observer

+ * The SplObserver to detach. + *

+ * @return void + */ + public function detach(SplObserver $observer) + { + $key = array_search($observer, $this->observer, true); + if ($key !== false) { + unset($this->observer[$key]); + } + } + + /** + * (PHP 5 >= 5.1.0)
+ * Notify an observer + * @link http://php.net/manual/en/splsubject.notify.php + * @return void + */ + public function notify() + { + /** @var SplObserver $observer */ + $observer = null; + foreach ($this->observer as $observer) { + $observer->update($this); + } + } + + /** + * (PHP 5 >= 5.1.0)
+ * Count elements of an object + * @link http://php.net/manual/en/countable.count.php + * @return int The custom count as an integer. + *

+ *

+ * The return value is cast to an integer. + */ + public function count() + { + return count($this->preferences); + } + + /** + * Getter for change set + * @return ChangeSet + */ + public function getChangeSet() + { + return $this->changeSet; + } + + public function has($key) + { + return array_key_exists($key, $this->preferences); + } + + public function set($key, $value) + { + if ($this->has($key)) { + $oldValue = $this->get($key); + + // Do not notify useless writes + if ($oldValue !== $value) { + $this->changeSet->appendUpdate($key, $value); + } + } else { + $this->changeSet->appendCreate($key, $value); + } + + $this->processCommit(); + } + + public function get($key, $default = null) + { + if ($this->has($key)) { + return $this->preferences[$key]; + } + + return $default; + } + + public function remove($key) + { + if ($this->has($key)) { + $this->changeSet->appendDelete($key); + $this->processCommit(); + return true; + } + + return false; + } + + public function startTransaction() + { + $this->autoCommit = false; + } + + public function commit() + { + $changeSet = $this->changeSet; + + if ($this->autoCommit === false) { + $this->autoCommit = true; + } + + if ($changeSet->hasChanges() === true) { + foreach ($changeSet->getCreate() as $key => $value) { + $this->preferences[$key] = $value; + } + + foreach ($changeSet->getUpdate() as $key => $value) { + $this->preferences[$key] = $value; + } + + foreach ($changeSet->getDelete() as $key) { + unset($this->preferences[$key]); + } + + $this->notify(); + + $this->changeSet->clear(); + } else { + throw new ProgrammingError('Nothing to commit'); + } + } + + private function processCommit() + { + if ($this->autoCommit === true) { + $this->commit(); + } + } } diff --git a/library/Icinga/User/Preferences/ChangeSet.php b/library/Icinga/User/Preferences/ChangeSet.php new file mode 100644 index 000000000..c50942eb1 --- /dev/null +++ b/library/Icinga/User/Preferences/ChangeSet.php @@ -0,0 +1,129 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Icinga\User\Preferences; + +/** + * Voyager object to transport changes between consumers + */ +class ChangeSet +{ + /** + * Items to update + * + * @var array + */ + private $update = array(); + + /** + * Items to delete + * + * @var array + */ + private $delete = array(); + + /** + * Items to create + * + * @var array + */ + private $create = array(); + + /** + * Push an update to stack + * @param string $key + * @param mixed $value + */ + public function appendUpdate($key, $value) + { + $this->update[$key] = $value; + } + + /** + * Getter for pending updates + * @return array + */ + public function getUpdate() + { + return $this->update; + } + + /** + * Push delete operation to stack + * + * @param string $key + */ + public function appendDelete($key) + { + $this->delete[] = $key; + } + + /** + * @return array + */ + public function getDelete() + { + return $this->delete; + } + + /** + * Push create operation to stack + * + * @param string $key + * @param mixed $value + */ + public function appendCreate($key, $value) + { + $this->create[$key] = $value; + } + + public function getCreate() + { + return $this->create; + } + + /** + * Clear all changes + */ + public function clear() + { + $this->update = array(); + $this->delete = array(); + $this->create = array(); + } + + /** + * Test for registered changes + * + * @return bool + */ + public function hasChanges() + { + return (count($this->update) > 0) || (count($this->delete) > 0) || (count($this->create)); + } +} diff --git a/library/Icinga/User/Preferences/FlushObserverInterface.php b/library/Icinga/User/Preferences/FlushObserverInterface.php new file mode 100644 index 000000000..5499e1656 --- /dev/null +++ b/library/Icinga/User/Preferences/FlushObserverInterface.php @@ -0,0 +1,45 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Icinga\User\Preferences; + +use Icinga\User; +use \SplObserver; + +/** + * Handle preference updates + */ +interface FlushObserverInterface extends SplObserver +{ + /** + * Setter for user + * + * @param User $user + */ + public function setUser(User $user); +} diff --git a/library/Icinga/User/Preferences/IniStore.php b/library/Icinga/User/Preferences/IniStore.php new file mode 100644 index 000000000..f455eb738 --- /dev/null +++ b/library/Icinga/User/Preferences/IniStore.php @@ -0,0 +1,202 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Icinga\User\Preferences; + +use \SplObserver; +use \SplSubject; +use Icinga\User; +use Icinga\User\Preferences; +use Icinga\Exception\ConfigurationError; +use Icinga\Exception\ProgrammingError; +use \Zend_Config; +use \Zend_Config_Writer_Ini; + +/** + * Handle preferences in ini files + * + * Load and write values from user preferences to ini files + */ +class IniStore implements LoadInterface, FlushObserverInterface +{ + /** + * Name of preferences section in ini file + */ + const DEFAULT_SECTION = 'preferences'; + + /** + * Path to ini configuration + * + * @var string + */ + private $configDir; + + /** + * Specific user file for preferences + * + * @var string + */ + private $preferencesFile; + + /** + * Config container + * + * @var Zend_Config + */ + private $iniConfig; + + /** + * Ini writer + * + * @var Zend_Config_Writer_Ini + */ + private $iniWriter; + + /** + * Current user + * + * @var User + */ + private $user; + + /** + * Create a new object + * + * @param string $configDir + */ + public function __construct($configDir) + { + $this->setConfigDir($configDir); + } + + /** + * Setter for config directory + * + * @param string $configDir + * @throws \Icinga\Exception\ConfigurationError + */ + public function setConfigDir($configDir) + { + if (!is_dir($configDir)) { + throw new ConfigurationError('Config dir dos not exist: '. $configDir); + } + + $this->configDir = $configDir; + } + + /** + * Setter for user + * + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + + $this->preferencesFile = sprintf( + '%s/%s.ini', + $this->configDir, + $this->user->getUsername() + ); + + if (file_exists($this->preferencesFile) === false) { + $this->createDefaultIniFile(); + } + + $this->iniConfig = new Zend_Config( + parse_ini_file($this->preferencesFile), + true + ); + + $this->iniWriter = new Zend_Config_Writer_Ini( + array( + 'config' => $this->iniConfig, + 'filename' => $this->preferencesFile + ) + ); + } + + /** + * Helper to create blank ini file + */ + private function createDefaultIniFile() + { + $writer = new Zend_Config_Writer_Ini( + array( + 'config' => new Zend_Config( + array( + self::DEFAULT_SECTION => array() + ) + ), + 'filename' => $this->preferencesFile + ) + ); + $writer->write(); + } + + /** + * Load preferences from source + * + * @return array + */ + public function load() + { + return $this->iniConfig->toArray(); + } + + /** + * Receive update from subject + * + * @link http://php.net/manual/en/splobserver.update.php + * @param SplSubject $subject + * @throws ProgrammingError + */ + public function update(SplSubject $subject) + { + if (!$subject instanceof Preferences) { + throw new ProgrammingError('Not compatible with '. get_class($subject)); + } + + $changeSet = $subject->getChangeSet(); + + foreach ($changeSet->getCreate() as $key => $value) { + $this->iniConfig->{$key} = $value; + } + + foreach ($changeSet->getUpdate() as $key => $value) { + $this->iniConfig->{$key} = $value; + } + + foreach ($changeSet->getDelete() as $key) { + unset($this->iniConfig->{$key}); + } + + // Persist changes to disk + $this->iniWriter->write(); + } +} diff --git a/library/Icinga/User/Preferences/LoadInterface.php b/library/Icinga/User/Preferences/LoadInterface.php new file mode 100644 index 000000000..9066d164d --- /dev/null +++ b/library/Icinga/User/Preferences/LoadInterface.php @@ -0,0 +1,42 @@ + + * @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2 + * @author Icinga Development Team + */ +// {{{ICINGA_LICENSE_HEADER}}} + +namespace Icinga\User\Preferences; + +/** + * Describe how to load preferences from data sources + */ +interface LoadInterface +{ + /** + * Load preferences from source + * + * @return array + */ + public function load(); +} diff --git a/test/php/library/Icinga/User/Preferences/ChangeSetTest.php b/test/php/library/Icinga/User/Preferences/ChangeSetTest.php new file mode 100644 index 000000000..02b7e584d --- /dev/null +++ b/test/php/library/Icinga/User/Preferences/ChangeSetTest.php @@ -0,0 +1,93 @@ +appendCreate('test.key1', 'ok1'); + $changeSet->appendCreate('test.key2', 'ok2'); + + $creates = $changeSet->getCreate(); + + $this->assertCount(2, $creates); + $this->assertTrue($changeSet->hasChanges()); + + $this->assertEquals( + array( + 'test.key1' => 'ok1', + 'test.key2' => 'ok2' + ), + $creates + ); + } + + public function testAppendUpdate() + { + $changeSet = new ChangeSet(); + $changeSet->appendUpdate('test.key3', 'ok1'); + $changeSet->appendUpdate('test.key4', 'ok2'); + $changeSet->appendUpdate('test.key5', 'ok3'); + + $updates = $changeSet->getUpdate(); + + $this->assertCount(3, $updates); + $this->assertTrue($changeSet->hasChanges()); + + $this->assertEquals( + array( + 'test.key3' => 'ok1', + 'test.key4' => 'ok2', + 'test.key5' => 'ok3' + ), + $updates + ); + } + + public function testAppendDelete() + { + $changeSet = new ChangeSet(); + $changeSet->appendDelete('test.key6'); + $changeSet->appendDelete('test.key7'); + $changeSet->appendDelete('test.key8'); + $changeSet->appendDelete('test.key9'); + + $deletes = $changeSet->getDelete(); + + $this->assertCount(4, $deletes); + $this->assertTrue($changeSet->hasChanges()); + + $this->assertEquals( + array( + 'test.key6', + 'test.key7', + 'test.key8', + 'test.key9', + ), + $deletes + ); + } + + public function testObjectReset() + { + $changeSet = new ChangeSet(); + $changeSet->appendCreate('test.key1', 'ok'); + $changeSet->appendCreate('test.key2', 'ok'); + $changeSet->appendUpdate('test.key3', 'ok'); + $changeSet->appendUpdate('test.key4', 'ok'); + $changeSet->appendUpdate('test.key5', 'ok'); + $changeSet->appendDelete('test.key6'); + $changeSet->appendDelete('test.key7'); + $changeSet->appendDelete('test.key8'); + + $this->assertTrue($changeSet->hasChanges()); + $changeSet->clear(); + $this->assertFalse($changeSet->hasChanges()); + } +} \ No newline at end of file diff --git a/test/php/library/Icinga/User/Preferences/IniStoreTest.php b/test/php/library/Icinga/User/Preferences/IniStoreTest.php new file mode 100644 index 000000000..99412531c --- /dev/null +++ b/test/php/library/Icinga/User/Preferences/IniStoreTest.php @@ -0,0 +1,130 @@ +tempDir = tempnam($tempDir, 'ini-store-test'); + if (file_exists($this->tempDir)) { + unlink($this->tempDir); + } + mkdir($this->tempDir); + } + + protected function tearDown() + { + if (is_dir($this->tempDir)) { + system('rm -rf '. $this->tempDir); + } + } + + private function createTestConfig() + { + $user = new User('jdoe'); + $iniStore = new IniStore($this->tempDir); + $iniStore->setUser($user); + + $preferences = new User\Preferences(array()); + $preferences->attach($iniStore); + + $preferences->startTransaction(); + $preferences->set('test.key1', 'ok1'); + $preferences->set('test.key2', 'ok2'); + $preferences->set('test.key3', 'ok3'); + $preferences->set('test.key4', 'ok4'); + $preferences->commit(); + + return $preferences; + } + + public function testWritePreferencesToFile() + { + $user = new User('jdoe'); + $iniStore = new IniStore($this->tempDir); + $iniStore->setUser($user); + + $preferences = new User\Preferences(array()); + $preferences->attach($iniStore); + + $preferences->startTransaction(); + $preferences->set('test.key1', 'ok1'); + $preferences->set('test.key2', 'ok2'); + $preferences->set('test.key3', 'ok3'); + $preferences->commit(); + + $preferences->remove('test.key2'); + + $file = $this->tempDir. '/jdoe.ini'; + $data = (object)parse_ini_file($file); + + $this->assertAttributeEquals('ok1', 'test.key1', $data, 'ini contains test.key1'); + $this->assertAttributeEquals('ok3', 'test.key3', $data, 'ini contains test.key3'); + $this->assertObjectNotHasAttribute('test.key2', $data, 'ini does not contain key test.key2'); + } + + public function testUpdatePreferencesToFile() + { + $this->createTestConfig(); + + $user = new User('jdoe'); + $iniStore = new IniStore($this->tempDir); + $iniStore->setUser($user); + + $preferences = new User\Preferences($iniStore->load()); + $preferences->attach($iniStore); + + $preferences->startTransaction(); + $preferences->remove('test.key1'); + $preferences->remove('test.key2'); + $preferences->remove('test.key3'); + $preferences->set('test.key4', 'ok9898'); + + $this->assertCount(4, $preferences, 'Before commit we need 4 items'); + + $preferences->commit(); + $this->assertCount(1, $preferences, 'After we need 1 item'); + + $this->assertEquals('ok9898', $preferences->get('test.key4'), 'After commit preference key has changed'); + } + + public function testLoadInterface() + { + $this->createTestConfig(); + + $user = new User('jdoe'); + $iniStore = new IniStore($this->tempDir); + $iniStore->setUser($user); + + $preferences = new User\Preferences($iniStore->load()); + $this->assertEquals('ok4', $preferences->get('test.key4'), 'Test for test.key4'); + $this->assertCount(4, $preferences, 'Count 4 items'); + } + + /** + * @expectedException Icinga\Exception\ConfigurationError + * @expectedExceptionMessage Config dir dos not exist: /path/does/not/exist + */ + public function testInitializationFailure() + { + $iniStore = new IniStore('/path/does/not/exist'); + } +} \ No newline at end of file diff --git a/test/php/library/Icinga/User/PreferencesTest.php b/test/php/library/Icinga/User/PreferencesTest.php new file mode 100644 index 000000000..1455d521d --- /dev/null +++ b/test/php/library/Icinga/User/PreferencesTest.php @@ -0,0 +1,93 @@ + 'ok1', + 'test.key2' => 'ok2' + ) + ); + + $this->assertCount(2, $preferences); + + $this->assertEquals('ok2', $preferences->get('test.key2')); + } + + public function testGetDefaultValues() + { + $preferences = new Preferences(array()); + $preferences->set('test.key223', 'ok223'); + $preferences->set('test.key333', 'ok333'); + + $this->assertCount(2, $preferences); + + $this->assertEquals('ok223', $preferences->get('test.key223')); + + $this->assertNull($preferences->get('does.not.exist')); + + $this->assertEquals(123123, $preferences->get('does.not.exist', 123123)); + } + + public function testTransactionalCommit() + { + $preferences = new Preferences(array()); + $preferences->startTransaction(); + + $preferences->set('test.key1', 'ok1'); + $preferences->set('test.key2', 'ok2'); + + $this->assertCount(0, $preferences); + $this->assertCount(2, $preferences->getChangeSet()->getCreate()); + + $preferences->commit(); + + $this->assertCount(2, $preferences); + + $preferences->startTransaction(); + + $preferences->remove('test.key2'); + $this->assertEquals('ok2', $preferences->get('test.key2')); + $this->assertCount(1, $preferences->getChangeSet()->getDelete()); + + $preferences->commit(); + $this->assertNull($preferences->get('test.key2')); + } + + /** + * @expectedException Icinga\Exception\ProgrammingError + * @expectedExceptionMessage Nothing to commit + */ + public function testNothingToCommitException() + { + $preferences = new Preferences(array()); + $preferences->commit(); + } + + public function testSetCreateOrUpdate() + { + $preferences = new Preferences(array()); + + $preferences->startTransaction(); + $preferences->set('test.key1', 'ok1'); + $this->assertCount(1, $preferences->getChangeSet()->getCreate()); + $this->assertCount(0, $preferences->getChangeSet()->getUpdate()); + $preferences->commit(); + + $preferences->startTransaction(); + $preferences->set('test.key1', 'ok2'); + $this->assertCount(0, $preferences->getChangeSet()->getCreate()); + $this->assertCount(1, $preferences->getChangeSet()->getUpdate()); + $preferences->commit(); + } +} \ No newline at end of file