User preferences: Add database store

refs #4069
This commit is contained in:
Marius Hein 2013-08-02 14:58:36 +02:00
parent f3ed73175b
commit 6112189b0c
8 changed files with 494 additions and 7 deletions

View File

@ -18,5 +18,15 @@ debug.enable = 1
debug.type = stream
debug.target = /tmp/icinga2.debug.log
; Use ini store to store preferences on local disk
[preferences]
type=ini
; Use database to store preference into mysql or postgres
;[preferences]
;type=db
;dbtype=pgsql
;dbhost=127.0.0.1
;dbpassword=icingaweb
;dbuser=icingaweb
;dbname=icingaweb

View File

@ -0,0 +1,7 @@
create table `preferences`(
`username` VARCHAR(255) NOT NULL,
`preference` VARCHAR(100) NOT NULL,
`value` VARCHAR(255) NOT NULL,
PRIMARY KEY(username, preference)
);

View File

@ -0,0 +1,7 @@
create table "preferences"(
"username" VARCHAR(255) NOT NULL,
"preference" VARCHAR(100) NOT NULL,
"value" VARCHAR(255) NOT NULL,
PRIMARY KEY(username, preference)
);

View File

@ -29,6 +29,7 @@
namespace Icinga\Application;
use Icinga\Authentication\Manager as AuthenticationManager;
use Icinga\Exception\ConfigurationError;
use Icinga\User\Preferences;
use Icinga\User;
use Icinga\Web\Request;
@ -40,7 +41,7 @@ use Zend_Controller_Action_HelperBroker;
use Zend_Controller_Router_Route;
use Zend_Controller_Action_Helper_ViewRenderer;
use Icinga\Web\View;
use Icinga\User\Preferences\StorageFactory;
use Icinga\User\Preferences\StoreFactory;
use Icinga\User\Preferences\SessionStore;
/**
@ -201,6 +202,12 @@ class Web extends ApplicationBootstrap
return $this;
}
/**
* Create user object and inject preference interface
*
* @throws ConfigurationError
* @return User
*/
private function setupUser()
{
$authenticationManager = AuthenticationManager::getInstance(
@ -211,17 +218,24 @@ class Web extends ApplicationBootstrap
);
if ($authenticationManager->isAuthenticated() === true) {
if ($this->getConfig()->preferences === null) {
throw new ConfigurationError('Preferences not configured in config.ini');
}
$user = $authenticationManager->getUser();
$this->getConfig()->preferences->configPath = $this->getConfigDir('preferences');
$preferenceStore = StorageFactory::create(
$preferenceStore = StoreFactory::create(
$this->getConfig()->preferences,
$user
);
// Needed to update values in user session
$sessionStore = new SessionStore($authenticationManager->getSession());
// Performance: Do not ask provider if we've preferences
// stored in session
$initialPreferences = (count($sessionStore->load()))
? $sessionStore->load() : $preferenceStore->load();
@ -232,6 +246,7 @@ class Web extends ApplicationBootstrap
$user->setPreferences($preferences);
// TESTING
$requestCounter = $user->getPreferences()->get('test.request.counter', 0);
$requestCounter++;
$user->getPreferences()->set('test.request.counter', $requestCounter);

View File

@ -34,21 +34,21 @@ namespace Icinga\User\Preferences;
class ChangeSet
{
/**
* Items to update
* Stack of pending updates
*
* @var array
*/
private $update = array();
/**
* Items to delete
* Stack of pending delete operations
*
* @var array
*/
private $delete = array();
/**
* Items to create
* Stack of pending create operations
*
* @var array
*/
@ -56,6 +56,7 @@ class ChangeSet
/**
* Push an update to stack
*
* @param string $key
* @param mixed $value
*/
@ -66,6 +67,7 @@ class ChangeSet
/**
* Getter for pending updates
*
* @return array
*/
public function getUpdate()
@ -84,6 +86,8 @@ class ChangeSet
}
/**
* Get pending delete operations
*
* @return array
*/
public function getDelete()
@ -95,13 +99,18 @@ class ChangeSet
* Push create operation to stack
*
* @param string $key
* @param mixed $value
* @param mixed $value
*/
public function appendCreate($key, $value)
{
$this->create[$key] = $value;
}
/**
* Get pending create operations
*
* @return array
*/
public function getCreate()
{
return $this->create;

View File

@ -0,0 +1,241 @@
<?php
// {{{ICINGA_LICENSE_HEADER}}}
/**
* This file is part of Icinga 2 Web.
*
* Icinga 2 Web - Head for multiple monitoring backends.
* Copyright (C) 2013 Icinga Development Team
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* @copyright 2013 Icinga Development Team <info@icinga.org>
* @license http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
* @author Icinga Development Team <info@icinga.org>
*/
// {{{ICINGA_LICENSE_HEADER}}}
namespace Icinga\User\Preferences;
use Icinga\User;
use SplSubject;
use Zend_Db_Adapter_Abstract;
use Icinga\Exception\ProgrammingError;
use Icinga\User\Preferences;
/**
* Store user preferences in database
*/
class DbStore implements LoadInterface, FlushObserverInterface
{
/**
* Column name for username
*/
const COLUMN_USERNAME = 'username';
/**
* Column name for preference
*/
const COLUMN_PREFERENCE = 'preference';
/**
* Column name for value
*/
const COLUMN_VALUE = 'value';
/**
* User object
*
* @var User
*/
private $user;
/**
* Zend database adapter
*
* @var Zend_Db_Adapter_Abstract
*/
private $dbAdapter;
/**
* Table name
*
* @var string
*/
private $table = 'preferences';
/**
* Setter for user
*
* @param User $user
*/
public function setUser(User $user)
{
$this->user = $user;
}
/**
* Setter for db adapter
*
* @param Zend_Db_Adapter_Abstract $dbAdapter
*/
public function setDbAdapter(Zend_Db_Adapter_Abstract $dbAdapter)
{
$this->dbAdapter = $dbAdapter;
$this->dbAdapter->getProfiler()->setEnabled(true);
}
/**
* Setter for table
*
* @param string $table
*/
public function setTable($table)
{
$this->table = $table;
}
/**
* Load preferences from source
*
* @return array
*/
public function load()
{
$res = $this->dbAdapter->select()->from($this->table)
->where('username=?', $this->user->getUsername())
->query();
$out = array();
foreach ($res->fetchAll() as $row) {
$out[$row[self::COLUMN_PREFERENCE]] = $row[self::COLUMN_VALUE];
}
return $out;
}
/**
* Helper to create zend db suitable where condition
*
* @param string $preference
* @return array
*/
private function createWhereCondition($preference)
{
return array(
self::COLUMN_USERNAME. '=?' => $this->user->getUsername(),
self::COLUMN_PREFERENCE. '=?' => $preference
);
}
/**
* Create operation
*
* @param string $preference
* @param mixed $value
* @return int
*/
private function doCreate($preference, $value)
{
return $this->dbAdapter->insert(
$this->table,
array(
self::COLUMN_USERNAME => $this->user->getUsername(),
self::COLUMN_PREFERENCE => $preference,
self::COLUMN_VALUE => $value
)
);
}
/**
* Update operation
*
* @param string $preference
* @param mixed $value
* @return int
*/
private function doUpdate($preference, $value)
{
return $this->dbAdapter->update(
$this->table,
array(
self::COLUMN_VALUE => $value
),
$this->createWhereCondition($preference)
);
}
/**
* Delete preference operation
*
* @param string $preference
* @return int
*/
private function doDelete($preference)
{
return $this->dbAdapter->delete(
$this->table,
$this->createWhereCondition($preference)
);
}
/**
* 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) {
$retVal = $this->doCreate($key, $value);
if (!$retVal) {
throw new ProgrammingError('Could not create preference value in db: '. $key. '='. $value);
}
}
foreach ($changeSet->getUpdate() as $key => $value) {
$retVal = $this->doUpdate($key, $value);
/*
* Fallback if we switch storage type while user logged in
*/
if (!$retVal) {
$retVal = $this->doCreate($key, $value);
if (!$retVal) {
throw new ProgrammingError('Could not create preference value in db: '. $key. '='. $value);
}
}
}
foreach ($changeSet->getDelete() as $key) {
$retVal = $this->doDelete($key);
if (!$retVal) {
throw new ProgrammingError('Could not delete preference value in db: '. $key);
}
}
}
}

View File

@ -31,8 +31,12 @@ namespace Icinga\User\Preferences;
use Icinga\User;
use Icinga\Exception\ProgrammingError;
use \Zend_Config;
use \Zend_Db;
final class StorageFactory
/**
* Create preference stores from zend config
*/
final class StoreFactory
{
/**
* Prefix for classes containing namespace
@ -66,6 +70,31 @@ final class StorageFactory
$items = $config->toArray();
unset($items['type']);
// TODO(mh): Encapsulate into a db adapter factory (#4503)
if (isset($items['dbname'])
&& isset($items['dbuser'])
&& isset($items['dbpassword'])
&& isset($items['dbhost'])
&& isset($items['dbtype'])
) {
$zendDbType = 'PDO_'. strtoupper($items['dbtype']);
$zendDbOptions = array(
'host' => $items['dbhost'],
'username' => $items['dbuser'],
'password' => $items['dbpassword'],
'dbname' => $items['dbname']
);
if (isset($items['port'])) {
$zendDbOptions['port'] = $items['port'];
}
$dbAdapter = Zend_Db::factory($zendDbType, $zendDbOptions);
$items['dbAdapter'] = $dbAdapter;
}
foreach ($items as $key => $value) {
$setter = 'set'. ucfirst($key);
if (is_callable(array($store, $setter))) {

View File

@ -0,0 +1,169 @@
<?php
namespace Tests\Icinga\User\Preferences;
require_once __DIR__. '/../../../../../../library/Icinga/Exception/ConfigurationError.php';
require_once __DIR__. '/../../../../../../library/Icinga/User.php';
require_once __DIR__. '/../../../../../../library/Icinga/User/Preferences.php';
require_once __DIR__. '/../../../../../../library/Icinga/User/Preferences/LoadInterface.php';
require_once __DIR__. '/../../../../../../library/Icinga/User/Preferences/FlushObserverInterface.php';
require_once __DIR__. '/../../../../../../library/Icinga/User/Preferences/DbStore.php';
require_once 'Zend/Db.php';
require_once 'Zend/Db/Adapter/Abstract.php';
use Icinga\User;
use Icinga\User\Preferences\DbStore;
use Icinga\User\Preferences;
use \PHPUnit_Framework_TestCase;
use \Zend_Db;
use \Zend_Db_Adapter_Abstract;
use \PDOException;
use \Exception;
class DbStoreTest extends PHPUnit_Framework_TestCase
{
const TYPE_MYSQL = 'mysql';
const TYPE_PGSQL = 'pgsql';
private $database = 'icinga_unittest';
private $table = 'preferences';
private $databaseConfig = array(
'host' => '127.0.0.1',
'username' => 'icinga_unittest',
'password' => 'icinga_unittest',
'dbname' => 'icinga_unittest'
);
/**
* @var Zend_Db_Adapter_Abstract
*/
private $dbMysql;
/**
* @var Zend_Db_Adapter_Abstract
*/
private $dbPgsql;
private function createDb($type)
{
$zendType = 'PDO_'. strtoupper($type);
if ($type === self::TYPE_MYSQL) {
$this->databaseConfig['port'] = 3306;
} elseif ($type === self::TYPE_PGSQL) {
$this->databaseConfig['port'] = 5432;
}
$db = Zend_Db::factory(
$zendType,
$this->databaseConfig
);
try {
$db->getConnection();
$dumpFile = realpath(__DIR__. '/../../../../../../etc/schema/preferences.'. strtolower($type). '.sql');
if (!$dumpFile) {
throw new Exception('Dumpfile for db type not found: '. $type);
}
try {
$db->getConnection()->exec(file_get_contents($dumpFile));
} catch (PDOException $e) {
// PASS
}
} catch (\Zend_Db_Adapter_Exception $e) {
return null;
} catch (PDOException $e) {
return null;
}
return $db;
}
protected function setUp()
{
$this->dbMysql = $this->createDb(self::TYPE_MYSQL);
$this->dbPgsql = $this->createDb(self::TYPE_PGSQL);
}
protected function tearDown()
{
if ($this->dbMysql) {
$this->dbMysql->getConnection()->exec('DROP TABLE '. $this->table);
}
if ($this->dbPgsql) {
$this->dbPgsql->getConnection()->exec('DROP TABLE '. $this->table);
}
}
private function createDbStore(Zend_Db_Adapter_Abstract $db)
{
$user = new User('jdoe');
$store = new DbStore();
$store->setDbAdapter($db);
$store->setUser($user);
return $store;
}
public function testCreateUpdateDeletePreferenceValuesMySQL()
{
if ($this->dbMysql) {
$store = $this->createDbStore($this->dbMysql);
$preferences = new Preferences(array());
$preferences->attach($store);
$preferences->set('test.key1', 'OK1');
$preferences->set('test.key2', 'OK2');
$preferences->set('test.key3', 'OK2');
$preferences->remove('test.key2');
$preferences->set('test.key3', 'OKOK333');
$preferencesTest = new Preferences($store->load());
$this->assertEquals('OK1', $preferencesTest->get('test.key1'));
$this->assertNull($preferencesTest->get('test.key2'));
$this->assertEquals('OKOK333', $preferencesTest->get('test.key3'));
} else {
$this->markTestSkipped('MySQL test environment is not configured');
}
}
public function testCreateUpdateDeletePreferenceValuesPgSQL()
{
if ($this->dbPgsql) {
$store = $this->createDbStore($this->dbPgsql);
$preferences = new Preferences(array());
$preferences->attach($store);
$preferences->set('test.key1', 'OK1');
$preferences->set('test.key2', 'OK2');
$preferences->set('test.key3', 'OK2');
$preferences->remove('test.key2');
$preferences->set('test.key3', 'OKOK333');
$preferencesTest = new Preferences($store->load());
$this->assertEquals('OK1', $preferencesTest->get('test.key1'));
$this->assertNull($preferencesTest->get('test.key2'));
$this->assertEquals('OKOK333', $preferencesTest->get('test.key3'));
} else {
$this->markTestSkipped('PgSQL test environment is not configured');
}
}
}