Merge pull request #4765 from Icinga/drop-ini-backend-support

Drop ini backend support
This commit is contained in:
Johannes Meyer 2022-05-27 14:19:00 +02:00 committed by GitHub
commit db51fd79ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 355 additions and 680 deletions

View File

@ -69,9 +69,8 @@ class AccountController extends Controller
$form = new PreferenceForm(); $form = new PreferenceForm();
$form->setPreferences($user->getPreferences()); $form->setPreferences($user->getPreferences());
if ($config->get('config_backend', 'db') !== 'none' && isset($config->config_resource)) { if (isset($config->config_resource)) {
$form->setStore(PreferencesStore::create(new ConfigObject(array( $form->setStore(PreferencesStore::create(new ConfigObject(array(
'store' => $config->get('config_backend', 'db'),
'resource' => $config->config_resource 'resource' => $config->config_resource
)), $user)); )), $user));
} }

View File

@ -70,57 +70,23 @@ class ApplicationConfigForm extends Form
) )
); );
// we do not need this form for setup because we set the database there as default. $backends = array_keys(ResourceFactory::getResourceConfigs()->toArray());
// this form is only displayed in configuration -> application if preferences backend type of ini is recognized $backends = array_combine($backends, $backends);
if (isset($formData['global_config_backend']) && $formData['global_config_backend'] === 'ini') {
$this->addElement(
'select',
'global_config_backend',
[
'required' => true,
'autosubmit' => true,
'label' => $this->translate('User Preference Storage Type'),
'multiOptions' => [
'ini' => $this->translate('File System (INI Files)'),
'db' => $this->translate('Database')
]
]
);
} else {
$this->addElement(
'hidden',
'global_config_backend',
[
'required' => true,
'value' => 'db',
'disabled' => true
]
);
}
if (! isset($formData['global_config_backend']) || $formData['global_config_backend'] === 'db') { $this->addElement(
$backends = array(); 'select',
foreach (ResourceFactory::getResourceConfigs()->toArray() as $name => $resource) { 'global_config_resource',
if ($resource['type'] === 'db') { array(
$backends[$name] = $name; 'required' => true,
} 'multiOptions' => array_merge(
} ['' => sprintf(' - %s - ', $this->translate('Please choose'))],
$backends
$this->addElement( ),
'select', 'disable' => [''],
'global_config_resource', 'value' => '',
array( 'label' => $this->translate('Configuration Database')
'required' => true, )
'multiOptions' => array_merge( );
['' => sprintf(' - %s - ', $this->translate('Please choose'))],
$backends
),
'disable' => [''],
'value' => '',
'label' => $this->translate('Configuration Database')
)
);
}
return $this; return $this;
} }

View File

@ -37,13 +37,4 @@ class GeneralConfigForm extends ConfigForm
$this->addSubForm($themingConfigForm->create($formData)); $this->addSubForm($themingConfigForm->create($formData));
$this->addSubForm($domainConfigForm->create($formData)); $this->addSubForm($domainConfigForm->create($formData));
} }
public function onRequest()
{
parent::onRequest();
if ($this->config->get('global', 'config_backend') === 'ini') {
$this->warning('The preferences backend of type INI is deprecated and will be removed with version 2.11');
}
}
} }

View File

@ -28,8 +28,7 @@ Option | Description
-------------------------|----------------------------------------------- -------------------------|-----------------------------------------------
show\_stacktraces | **Optional.** Whether to show debug stacktraces. Defaults to `0`. show\_stacktraces | **Optional.** Whether to show debug stacktraces. Defaults to `0`.
module\_path | **Optional.** Specifies the directories where modules can be installed. Multiple directories must be separated with colons. module\_path | **Optional.** Specifies the directories where modules can be installed. Multiple directories must be separated with colons.
config\_backend | **Optional.** Select the user preference storage. Can be set to `ini` (default), `db` or `none`. If `db` is selected, this requires the `config_resource` attribute. config\_resource | **Required.** Specify a defined [resource](04-Resources.md#resources-configuration-database) name.
config\_resource | **Optional.** Specify a defined [resource](04-Resources.md#resources-configuration-database) name. Can only be used if `config_backend` is set to `db`.
Example for storing the user preferences in the database resource `icingaweb_db`: Example for storing the user preferences in the database resource `icingaweb_db`:
@ -37,7 +36,6 @@ Example for storing the user preferences in the database resource `icingaweb_db`
``` ```
[global] [global]
show_stacktraces = "0" show_stacktraces = "0"
config_backend = "db"
config_resource = "icingaweb_db" config_resource = "icingaweb_db"
module_path = "/usr/share/icingaweb2/modules" module_path = "/usr/share/icingaweb2/modules"
``` ```

View File

@ -3,35 +3,13 @@
Preferences are settings a user can set for their account only, Preferences are settings a user can set for their account only,
for example the language and time zone. for example the language and time zone.
Preferences can be stored either in INI files or in a MySQL or in a PostgreSQL database. By default, Icinga Web 2 stores Preferences can be stored either in a MySQL or in a PostgreSQL database. The database must be configured.
preferences in INI files beneath Icinga Web 2's configuration directory.
```
/etc/icingaweb2/<username>/config.ini
```
## Configuration <a id="preferences-configuration"></a> ## Configuration <a id="preferences-configuration"></a>
The preference configuration backend is defined in the global [config.ini](03-Configuration.md#configuration-general-global) file. The preference configuration backend is defined in the global [config.ini](03-Configuration.md#configuration-general-global) file.
### Store Preferences in INI Files <a id="preferences-configuration-ini"></a> You have to define a [database resource](04-Resources.md#resources-configuration-database)
If preferences are stored in INI Files, Icinga Web 2 automatically creates one file per user using the username as
file name for storing preferences. A INI file is created once a user saves changed preferences the first time.
The files are located beneath the `preferences` directory beneath Icinga Web 2's configuration directory.
You need to add the following section to the global [config.ini](03-Configuration.md#configuration-general-global) file
in order to store preferences in a file.
```
[global]
config_backend = "ini"
```
### Store Preferences in a Database <a id="preferences-configuration-db"></a>
In order to be more flexible in distributed setups you can store preferences in a MySQL or in a PostgreSQL database.
For storing preferences in a database, you have to define a [database resource](04-Resources.md#resources-configuration-database)
which will be referenced as resource for the preferences storage. which will be referenced as resource for the preferences storage.
You need to add the following section to the global [config.ini](03-Configuration.md#configuration-general-global) file You need to add the following section to the global [config.ini](03-Configuration.md#configuration-general-global) file
@ -39,6 +17,5 @@ in order to store preferences in a database.
``` ```
[global] [global]
config_backend = "db"
config_resource = "icingaweb_db" config_resource = "icingaweb_db"
``` ```

View File

@ -6,6 +6,8 @@ v2.6 to v2.8 requires to follow the instructions for v2.7 too.
## Upgrading to Icinga Web 2 2.11.x ## Upgrading to Icinga Web 2 2.11.x
* The Vagrant file and all its assets have been removed. * The Vagrant file and all its assets have been removed.
* The `IniStore` class has been removed due to the deprecation of the Preferences ini backend.
* The `DbStore` class has been removed and its methods have been added to `PreferencesStore` class.
**Database Schema** **Database Schema**

View File

@ -376,26 +376,21 @@ class Auth
$config = new Config(); $config = new Config();
} }
if ($config->get('global', 'config_backend', 'db') !== 'none') { $preferencesConfig = new ConfigObject([
$preferencesConfig = new ConfigObject([ 'resource' => $config->get('global', 'config_resource')
'store' => $config->get('global', 'config_backend', 'db'), ]);
'resource' => $config->get('global', 'config_resource')
]);
try { try {
$preferencesStore = PreferencesStore::create($preferencesConfig, $user); $preferencesStore = PreferencesStore::create($preferencesConfig, $user);
$preferences = new Preferences($preferencesStore->load()); $preferences = new Preferences($preferencesStore->load());
} catch (Exception $e) { } catch (Exception $e) {
Logger::error( Logger::error(
new IcingaException( new IcingaException(
'Cannot load preferences for user "%s". An exception was thrown: %s', 'Cannot load preferences for user "%s". An exception was thrown: %s',
$user->getUsername(), $user->getUsername(),
$e $e
) )
); );
$preferences = new Preferences();
}
} else {
$preferences = new Preferences(); $preferences = new Preferences();
} }

View File

@ -3,18 +3,21 @@
namespace Icinga\User\Preferences; namespace Icinga\User\Preferences;
use Icinga\Application\Config; use Exception;
use Icinga\Application\Logger; use Icinga\Exception\NotReadableError;
use Icinga\Exception\NotWritableError;
use Icinga\User; use Icinga\User;
use Icinga\User\Preferences; use Icinga\User\Preferences;
use Icinga\Data\ConfigObject; use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory; use Icinga\Data\ResourceFactory;
use Icinga\Exception\ConfigurationError; use Icinga\Exception\ConfigurationError;
use Icinga\Data\Db\DbConnection; use Zend_Db_Expr;
/** /**
* Preferences store factory * Preferences store factory
* *
* Load and save user preferences by using a database
*
* Usage example: * Usage example:
* <code> * <code>
* <?php * <?php
@ -23,11 +26,10 @@ use Icinga\Data\Db\DbConnection;
* use Icinga\User\Preferences; * use Icinga\User\Preferences;
* use Icinga\User\Preferences\PreferencesStore; * use Icinga\User\Preferences\PreferencesStore;
* *
* // Create a INI store * // Create a db store
* $store = PreferencesStore::create( * $store = PreferencesStore::create(
* new ConfigObject( * new ConfigObject(
* 'store' => 'ini', * 'resource' => 'resource name'
* 'config_path' => '/path/to/preferences'
* ), * ),
* $user // Instance of \Icinga\User * $user // Instance of \Icinga\User
* ); * );
@ -37,8 +39,52 @@ use Icinga\Data\Db\DbConnection;
* $store->save($preferences); * $store->save($preferences);
* </code> * </code>
*/ */
abstract class PreferencesStore class PreferencesStore
{ {
/**
* Column name for username
*/
const COLUMN_USERNAME = 'username';
/**
* Column name for section
*/
const COLUMN_SECTION = 'section';
/**
* Column name for preference
*/
const COLUMN_PREFERENCE = 'name';
/**
* Column name for value
*/
const COLUMN_VALUE = 'value';
/**
* Column name for created time
*/
const COLUMN_CREATED_TIME = 'ctime';
/**
* Column name for modified time
*/
const COLUMN_MODIFIED_TIME = 'mtime';
/**
* Table name
*
* @var string
*/
protected $table = 'icingaweb_user_preference';
/**
* Stored preferences
*
* @var array
*/
protected $preferences = [];
/** /**
* Store config * Store config
* *
@ -71,7 +117,7 @@ abstract class PreferencesStore
* *
* @return ConfigObject * @return ConfigObject
*/ */
public function getStoreConfig() public function getStoreConfig(): ConfigObject
{ {
return $this->config; return $this->config;
} }
@ -81,7 +127,7 @@ abstract class PreferencesStore
* *
* @return User * @return User
*/ */
public function getUser() public function getUser(): User
{ {
return $this->user; return $this->user;
} }
@ -89,21 +135,190 @@ abstract class PreferencesStore
/** /**
* Initialize the store * Initialize the store
*/ */
abstract protected function init(); protected function init(): void
{
}
/** /**
* Load preferences from source * Load preferences from the database
* *
* @return array * @return array
*
* @throws NotReadableError In case the database operation failed
*/ */
abstract public function load(); public function load(): array
{
try {
$select = $this->getStoreConfig()->connection->getDbAdapter()->select();
$result = $select
->from($this->table, [self::COLUMN_SECTION, self::COLUMN_PREFERENCE, self::COLUMN_VALUE])
->where(self::COLUMN_USERNAME . ' = ?', $this->getUser()->getUsername())
->query()
->fetchAll();
} catch (Exception $e) {
throw new NotReadableError(
'Cannot fetch preferences for user %s from database',
$this->getUser()->getUsername(),
$e
);
}
if ($result !== false) {
$values = [];
foreach ($result as $row) {
$values[$row->{self::COLUMN_SECTION}][$row->{self::COLUMN_PREFERENCE}] = $row->{self::COLUMN_VALUE};
}
$this->preferences = $values;
}
return $this->preferences;
}
/** /**
* Save the given preferences * Save the given preferences in the database
* *
* @param Preferences $preferences The preferences to save * @param Preferences $preferences The preferences to save
*/ */
abstract public function save(Preferences $preferences); public function save(Preferences $preferences): void
{
$preferences = $preferences->toArray();
$sections = array_keys($preferences);
foreach ($sections as $section) {
if (! array_key_exists($section, $this->preferences)) {
$this->preferences[$section] = [];
}
if (! array_key_exists($section, $preferences)) {
$preferences[$section] = [];
}
$toBeInserted = array_diff_key($preferences[$section], $this->preferences[$section]);
if (!empty($toBeInserted)) {
$this->insert($toBeInserted, $section);
}
$toBeUpdated = array_intersect_key(
array_diff_assoc($preferences[$section], $this->preferences[$section]),
array_diff_assoc($this->preferences[$section], $preferences[$section])
);
if (!empty($toBeUpdated)) {
$this->update($toBeUpdated, $section);
}
$toBeDeleted = array_keys(array_diff_key($this->preferences[$section], $preferences[$section]));
if (!empty($toBeDeleted)) {
$this->delete($toBeDeleted, $section);
}
}
}
/**
* Insert the given preferences into the database
*
* @param array $preferences The preferences to insert
* @param string $section The preferences in section to update
*
* @throws NotWritableError In case the database operation failed
*/
protected function insert(array $preferences, string $section): void
{
/** @var \Zend_Db_Adapter_Abstract $db */
$db = $this->getStoreConfig()->connection->getDbAdapter();
try {
foreach ($preferences as $key => $value) {
$db->insert(
$this->table,
[
self::COLUMN_USERNAME => $this->getUser()->getUsername(),
$db->quoteIdentifier(self::COLUMN_SECTION) => $section,
$db->quoteIdentifier(self::COLUMN_PREFERENCE) => $key,
self::COLUMN_VALUE => $value,
self::COLUMN_CREATED_TIME => new Zend_Db_Expr('NOW()'),
self::COLUMN_MODIFIED_TIME => new Zend_Db_Expr('NOW()')
]
);
}
} catch (Exception $e) {
throw new NotWritableError(
'Cannot insert preferences for user %s into database',
$this->getUser()->getUsername(),
$e
);
}
}
/**
* Update the given preferences in the database
*
* @param array $preferences The preferences to update
* @param string $section The preferences in section to update
*
* @throws NotWritableError In case the database operation failed
*/
protected function update(array $preferences, string $section): void
{
/** @var \Zend_Db_Adapter_Abstract $db */
$db = $this->getStoreConfig()->connection->getDbAdapter();
try {
foreach ($preferences as $key => $value) {
$db->update(
$this->table,
[
self::COLUMN_VALUE => $value,
self::COLUMN_MODIFIED_TIME => new Zend_Db_Expr('NOW()')
],
[
self::COLUMN_USERNAME . '=?' => $this->getUser()->getUsername(),
$db->quoteIdentifier(self::COLUMN_SECTION) . '=?' => $section,
$db->quoteIdentifier(self::COLUMN_PREFERENCE) . '=?' => $key
]
);
}
} catch (Exception $e) {
throw new NotWritableError(
'Cannot update preferences for user %s in database',
$this->getUser()->getUsername(),
$e
);
}
}
/**
* Delete the given preference names from the database
*
* @param array $preferenceKeys The preference names to delete
* @param string $section The preferences in section to update
*
* @throws NotWritableError In case the database operation failed
*/
protected function delete(array $preferenceKeys, string $section): void
{
/** @var \Zend_Db_Adapter_Abstract $db */
$db = $this->getStoreConfig()->connection->getDbAdapter();
try {
$db->delete(
$this->table,
[
self::COLUMN_USERNAME . '=?' => $this->getUser()->getUsername(),
$db->quoteIdentifier(self::COLUMN_SECTION) . '=?' => $section,
$db->quoteIdentifier(self::COLUMN_PREFERENCE) . ' IN (?)' => $preferenceKeys
]
);
} catch (Exception $e) {
throw new NotWritableError(
'Cannot delete preferences for user %s from database',
$this->getUser()->getUsername(),
$e
);
}
}
/** /**
* Create preferences storage adapter from config * Create preferences storage adapter from config
@ -115,29 +330,15 @@ abstract class PreferencesStore
* *
* @throws ConfigurationError When the configuration defines an invalid storage type * @throws ConfigurationError When the configuration defines an invalid storage type
*/ */
public static function create(ConfigObject $config, User $user) public static function create(ConfigObject $config, User $user): self
{ {
$type = ucfirst(strtolower($config->get('store', 'db'))); $resourceConfig = ResourceFactory::getResourceConfig($config->resource);
$storeClass = 'Icinga\\User\\Preferences\\Store\\' . $type . 'Store'; if ($resourceConfig->db === 'mysql') {
if (!class_exists($storeClass)) { $resourceConfig->charset = 'utf8mb4';
throw new ConfigurationError(
'Preferences configuration defines an invalid storage type. Storage type %s not found',
$type
);
} }
if ($type === 'Ini') { $config->connection = ResourceFactory::createResource($resourceConfig);
Logger::warning('The preferences backend of type INI is deprecated and will be removed with version 2.11');
$config->location = Config::resolvePath('preferences');
} elseif ($type === 'Db') {
$resourceConfig = ResourceFactory::getResourceConfig($config->resource);
if ($resourceConfig->db === 'mysql') {
$resourceConfig->charset = 'utf8mb4';
}
$config->connection = ResourceFactory::createResource($resourceConfig); return new self($config, $user);
}
return new $storeClass($config, $user);
} }
} }

View File

@ -1,255 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */
namespace Icinga\User\Preferences\Store;
use Exception;
use Icinga\Exception\NotReadableError;
use Icinga\Exception\NotWritableError;
use Icinga\User\Preferences;
use Icinga\User\Preferences\PreferencesStore;
use Zend_Db_Expr;
/**
* Load and save user preferences by using a database
*/
class DbStore extends PreferencesStore
{
/**
* Column name for username
*/
const COLUMN_USERNAME = 'username';
/**
* Column name for section
*/
const COLUMN_SECTION = 'section';
/**
* Column name for preference
*/
const COLUMN_PREFERENCE = 'name';
/**
* Column name for value
*/
const COLUMN_VALUE = 'value';
/**
* Column name for created time
*/
const COLUMN_CREATED_TIME = 'ctime';
/**
* Column name for modified time
*/
const COLUMN_MODIFIED_TIME = 'mtime';
/**
* Table name
*
* @var string
*/
protected $table = 'icingaweb_user_preference';
/**
* Stored preferences
*
* @var array
*/
protected $preferences = array();
/**
* Set the table to use
*
* @param string $table The table name
*/
public function setTable($table)
{
$this->table = $table;
}
/**
* Initialize the store
*/
protected function init()
{
}
/**
* Load preferences from the database
*
* @return array
*
* @throws NotReadableError In case the database operation failed
*/
public function load()
{
try {
$select = $this->getStoreConfig()->connection->getDbAdapter()->select();
$result = $select
->from($this->table, array(self::COLUMN_SECTION, self::COLUMN_PREFERENCE, self::COLUMN_VALUE))
->where(self::COLUMN_USERNAME . ' = ?', $this->getUser()->getUsername())
->query()
->fetchAll();
} catch (Exception $e) {
throw new NotReadableError(
'Cannot fetch preferences for user %s from database',
$this->getUser()->getUsername(),
$e
);
}
if ($result !== false) {
$values = array();
foreach ($result as $row) {
$values[$row->{self::COLUMN_SECTION}][$row->{self::COLUMN_PREFERENCE}] = $row->{self::COLUMN_VALUE};
}
$this->preferences = $values;
}
return $this->preferences;
}
/**
* Save the given preferences in the database
*
* @param Preferences $preferences The preferences to save
*/
public function save(Preferences $preferences)
{
$preferences = $preferences->toArray();
$sections = array_keys($preferences);
foreach ($sections as $section) {
if (! array_key_exists($section, $this->preferences)) {
$this->preferences[$section] = array();
}
if (! array_key_exists($section, $preferences)) {
$preferences[$section] = array();
}
$toBeInserted = array_diff_key($preferences[$section], $this->preferences[$section]);
if (!empty($toBeInserted)) {
$this->insert($toBeInserted, $section);
}
$toBeUpdated = array_intersect_key(
array_diff_assoc($preferences[$section], $this->preferences[$section]),
array_diff_assoc($this->preferences[$section], $preferences[$section])
);
if (!empty($toBeUpdated)) {
$this->update($toBeUpdated, $section);
}
$toBeDeleted = array_keys(array_diff_key($this->preferences[$section], $preferences[$section]));
if (!empty($toBeDeleted)) {
$this->delete($toBeDeleted, $section);
}
}
}
/**
* Insert the given preferences into the database
*
* @param array $preferences The preferences to insert
* @param string $section The preferences in section to update
*
* @throws NotWritableError In case the database operation failed
*/
protected function insert(array $preferences, $section)
{
/** @var \Zend_Db_Adapter_Abstract $db */
$db = $this->getStoreConfig()->connection->getDbAdapter();
try {
foreach ($preferences as $key => $value) {
$db->insert(
$this->table,
array(
self::COLUMN_USERNAME => $this->getUser()->getUsername(),
$db->quoteIdentifier(self::COLUMN_SECTION) => $section,
$db->quoteIdentifier(self::COLUMN_PREFERENCE) => $key,
self::COLUMN_VALUE => $value,
self::COLUMN_CREATED_TIME => new Zend_Db_Expr('NOW()'),
self::COLUMN_MODIFIED_TIME => new Zend_Db_Expr('NOW()')
)
);
}
} catch (Exception $e) {
throw new NotWritableError(
'Cannot insert preferences for user %s into database',
$this->getUser()->getUsername(),
$e
);
}
}
/**
* Update the given preferences in the database
*
* @param array $preferences The preferences to update
* @param string $section The preferences in section to update
*
* @throws NotWritableError In case the database operation failed
*/
protected function update(array $preferences, $section)
{
/** @var \Zend_Db_Adapter_Abstract $db */
$db = $this->getStoreConfig()->connection->getDbAdapter();
try {
foreach ($preferences as $key => $value) {
$db->update(
$this->table,
array(
self::COLUMN_VALUE => $value,
self::COLUMN_MODIFIED_TIME => new Zend_Db_Expr('NOW()')
),
array(
self::COLUMN_USERNAME . '=?' => $this->getUser()->getUsername(),
$db->quoteIdentifier(self::COLUMN_SECTION) . '=?' => $section,
$db->quoteIdentifier(self::COLUMN_PREFERENCE) . '=?' => $key
)
);
}
} catch (Exception $e) {
throw new NotWritableError(
'Cannot update preferences for user %s in database',
$this->getUser()->getUsername(),
$e
);
}
}
/**
* Delete the given preference names from the database
*
* @param array $preferenceKeys The preference names to delete
* @param string $section The preferences in section to update
*
* @throws NotWritableError In case the database operation failed
*/
protected function delete(array $preferenceKeys, $section)
{
/** @var \Zend_Db_Adapter_Abstract $db */
$db = $this->getStoreConfig()->connection->getDbAdapter();
try {
$db->delete(
$this->table,
array(
self::COLUMN_USERNAME . '=?' => $this->getUser()->getUsername(),
$db->quoteIdentifier(self::COLUMN_SECTION) . '=?' => $section,
$db->quoteIdentifier(self::COLUMN_PREFERENCE) . ' IN (?)' => $preferenceKeys
)
);
} catch (Exception $e) {
throw new NotWritableError(
'Cannot delete preferences for user %s from database',
$this->getUser()->getUsername(),
$e
);
}
}
}

View File

@ -1,118 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
namespace Icinga\User\Preferences\Store;
use Icinga\Application\Config;
use Icinga\Exception\NotReadableError;
use Icinga\Exception\NotWritableError;
use Icinga\User\Preferences;
use Icinga\User\Preferences\PreferencesStore;
use Icinga\File\Ini\IniParser;
/**
* Load and save user preferences from and to INI files
*/
class IniStore extends PreferencesStore
{
/**
* Preferences file of the given user
*
* @var string
*/
protected $preferencesFile;
/**
* Stored preferences
*
* @var array
*/
protected $preferences = array();
/**
* Initialize the store
*/
protected function init()
{
$this->preferencesFile = sprintf(
'%s/%s/config.ini',
$this->getStoreConfig()->location,
strtolower($this->getUser()->getUsername())
);
}
/**
* Load preferences from source
*
* @return array
*
* @throws NotReadableError When the INI file of the user exists and is not readable
*/
public function load()
{
if (file_exists($this->preferencesFile)) {
if (! is_readable($this->preferencesFile)) {
throw new NotReadableError(
'Preferences INI file %s for user %s is not readable',
$this->preferencesFile,
$this->getUser()->getUsername()
);
} else {
$this->preferences = IniParser::parseIniFile($this->preferencesFile)->toArray();
}
}
return $this->preferences;
}
/**
* Save the given preferences
*
* @param Preferences $preferences The preferences to save
*/
public function save(Preferences $preferences)
{
$this->preferences = $preferences->toArray();
// TODO: Elaborate whether we need to patch the contents
// $preferences = $preferences->toArray();
// $this->update(array_diff_assoc($preferences, $this->preferences));
// $this->delete(array_keys(array_diff_key($this->preferences, $preferences)));
$this->write();
}
/**
* Write the preferences
*
* @throws NotWritableError In case the INI file cannot be written
*/
public function write()
{
Config::fromArray($this->preferences)->saveIni($this->preferencesFile);
}
/**
* Add or update the given preferences
*
* @param array $preferences The preferences to set
*/
protected function update(array $preferences)
{
foreach ($preferences as $key => $value) {
$this->preferences[$key] = $value;
}
}
/**
* Delete the given preferences by name
*
* @param array $preferenceKeys The preference names to delete
*/
protected function delete(array $preferenceKeys)
{
foreach ($preferenceKeys as $key) {
unset($this->preferences[$key]);
}
}
}

View File

@ -10,9 +10,9 @@ use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory; use Icinga\Data\ResourceFactory;
use Icinga\Exception\NotReadableError; use Icinga\Exception\NotReadableError;
use Icinga\Exception\NotWritableError; use Icinga\Exception\NotWritableError;
use Icinga\File\Ini\IniParser;
use Icinga\User; use Icinga\User;
use Icinga\User\Preferences\Store\IniStore; use Icinga\User\Preferences\PreferencesStore;
use Icinga\User\Preferences\Store\DbStore;
use Icinga\Util\DirectoryIterator; use Icinga\Util\DirectoryIterator;
class PreferencesCommand extends Command class PreferencesCommand extends Command
@ -61,12 +61,15 @@ class PreferencesCommand extends Command
Logger::info('Migrating INI preferences for user "%s" to database...', $userName); Logger::info('Migrating INI preferences for user "%s" to database...', $userName);
$iniStore = new IniStore(new ConfigObject(['location' => $preferencesPath]), new User($userName)); $dbStore = new PreferencesStore(new ConfigObject(['connection' => $connection]), new User($userName));
$dbStore = new DbStore(new ConfigObject(['connection' => $connection]), new User($userName));
try { try {
$dbStore->load(); $dbStore->load();
$dbStore->save(new User\Preferences($iniStore->load())); $dbStore->save(
new User\Preferences(
$this->loadIniFile($preferencesPath, (new User($userName))->getUsername())
)
);
} catch (NotReadableError $e) { } catch (NotReadableError $e) {
if ($e->getPrevious() !== null) { if ($e->getPrevious() !== null) {
Logger::error('%s: %s', $e->getMessage(), $e->getPrevious()->getMessage()); Logger::error('%s: %s', $e->getMessage(), $e->getPrevious()->getMessage());
@ -89,7 +92,6 @@ class PreferencesCommand extends Command
if ($this->params->has('resource') && ! $this->params->has('no-set-config-backend')) { if ($this->params->has('resource') && ! $this->params->has('no-set-config-backend')) {
$appConfig = Config::app(); $appConfig = Config::app();
$globalConfig = $appConfig->getSection('global'); $globalConfig = $appConfig->getSection('global');
$globalConfig['config_backend'] = 'db';
$globalConfig['config_resource'] = $resource; $globalConfig['config_resource'] = $resource;
try { try {
@ -102,4 +104,28 @@ class PreferencesCommand extends Command
Logger::info('Successfully migrated all local user preferences to database'); Logger::info('Successfully migrated all local user preferences to database');
} }
private function loadIniFile(string $filePath, string $username): array
{
$preferences = [];
$preferencesFile = sprintf(
'%s/%s/config.ini',
$filePath,
strtolower($username)
);
if (file_exists($preferencesFile)) {
if (! is_readable($preferencesFile)) {
throw new NotReadableError(
'Preferences INI file %s for user %s is not readable',
$preferencesFile,
$username
);
} else {
$preferences = IniParser::parseIniFile($preferencesFile)->toArray();
}
}
return $preferences;
}
} }

View File

@ -178,80 +178,51 @@ class UserDomainMigration
{ {
$config = Config::app(); $config = Config::app();
$type = $config->get('global', 'config_backend', 'ini'); $resourceConfig = ResourceFactory::getResourceConfig($config->get('global', 'config_resource'));
if ($resourceConfig->db === 'mysql') {
$resourceConfig->charset = 'utf8mb4';
}
switch ($type) { /** @var DbConnection $conn */
case 'ini': $conn = ResourceFactory::createResource($resourceConfig);
$directory = Config::resolvePath('preferences');
$migration = array(); $query = $conn
->select()
->from('icingaweb_user_preference', array('username'))
->group('username');
if (DirectoryIterator::isReadable($directory)) { if ($this->map !== null) {
foreach (new DirectoryIterator($directory) as $username => $path) { $query->applyFilter(Filter::matchAny(Filter::where('username', array_keys($this->map))));
$user = new User($username); }
if (! $this->mustMigrate($user)) { $users = $query->fetchColumn();
continue;
}
$migrated = $this->migrateUser($user); $migration = array();
$migration[$path] = dirname($path) . '/' . $migrated->getUsername(); foreach ($users as $username) {
} $user = new User($username);
foreach ($migration as $from => $to) { if (! $this->mustMigrate($user)) {
rename($from, $to); continue;
} }
}
break; $migrated = $this->migrateUser($user);
case 'db':
$resourceConfig = ResourceFactory::getResourceConfig($config->get('global', 'config_resource'));
if ($resourceConfig->db === 'mysql') {
$resourceConfig->charset = 'utf8mb4';
}
/** @var DbConnection $conn */ $migration[$username] = $migrated->getUsername();
$conn = ResourceFactory::createResource($resourceConfig); }
$query = $conn if (! empty($migration)) {
->select() $conn->getDbAdapter()->beginTransaction();
->from('icingaweb_user_preference', array('username'))
->group('username');
if ($this->map !== null) { foreach ($migration as $originalUsername => $username) {
$query->applyFilter(Filter::matchAny(Filter::where('username', array_keys($this->map)))); $conn->update(
} 'icingaweb_user_preference',
array('username' => $username),
Filter::where('username', $originalUsername)
);
}
$users = $query->fetchColumn(); $conn->getDbAdapter()->commit();
$migration = array();
foreach ($users as $username) {
$user = new User($username);
if (! $this->mustMigrate($user)) {
continue;
}
$migrated = $this->migrateUser($user);
$migration[$username] = $migrated->getUsername();
}
if (! empty($migration)) {
$conn->getDbAdapter()->beginTransaction();
foreach ($migration as $originalUsername => $username) {
$conn->update(
'icingaweb_user_preference',
array('username' => $username),
Filter::where('username', $originalUsername)
);
}
$conn->getDbAdapter()->commit();
}
} }
} }

View File

@ -28,9 +28,7 @@ class GeneralConfigStep extends Step
$config[$section][$property] = $value; $config[$section][$property] = $value;
} }
if ($config['global']['config_backend'] === 'db') { $config['global']['config_resource'] = $this->data['resourceName'];
$config['global']['config_resource'] = $this->data['resourceName'];
}
try { try {
Config::fromArray($config) Config::fromArray($config)
@ -57,12 +55,7 @@ class GeneralConfigStep extends Step
? t('An exception\'s stacktrace is shown to every user by default.') ? t('An exception\'s stacktrace is shown to every user by default.')
: t('An exception\'s stacktrace is hidden from every user by default.') : t('An exception\'s stacktrace is hidden from every user by default.')
) . '</li>' ) . '</li>'
. '<li>' . sprintf( . '<li>' . t('Preferences will be stored using a database.') . '</li>'
$this->data['generalConfig']['global_config_backend'] === 'ini' ? sprintf(
t('Preferences will be stored per user account in INI files at: %s'),
Config::resolvePath('preferences')
) : t('Preferences will be stored using a database.')
) . '</li>'
. '</ul>'; . '</ul>';
$type = $this->data['generalConfig']['logging_log']; $type = $this->data['generalConfig']['logging_log'];

View File

@ -1,71 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
namespace Tests\Icinga\User\Preferences\Store;
use Mockery;
use Icinga\Data\ConfigObject;
use Icinga\Test\BaseTestCase;
use Icinga\User\Preferences\Store\IniStore;
class IniStoreWithSetGetPreferencesAndEmptyWrite extends IniStore
{
public function write()
{
// Gets called by IniStore::save
}
public function setPreferences($preferences)
{
$this->preferences = $preferences;
}
public function getPreferences()
{
return $this->preferences;
}
}
class IniStoreTest extends BaseTestCase
{
public function testWhetherPreferenceChangesAreApplied()
{
$store = $this->getStore();
$store->setPreferences(array('testsection' => array('key1' => '1')));
$store->save(
Mockery::mock('Icinga\User\Preferences', array(
'toArray' => array('testsection' => array('key1' => '11', 'key2' => '2'))
))
);
$this->assertEquals(
array('testsection' => array('key1' => '11', 'key2' => '2')),
$store->getPreferences(),
'IniStore::save does not properly apply changed preferences'
);
}
public function testWhetherPreferenceDeletionsAreApplied()
{
$store = $this->getStore();
$store->setPreferences(array('testsection' => array('key' => 'value')));
$store->save(Mockery::mock('Icinga\User\Preferences', array('toArray' => array('testsection' => array()))));
$result = $store->getPreferences();
$this->assertEmpty($result['testsection'], 'IniStore::save does not delete removed preferences');
}
protected function getStore()
{
return new IniStoreWithSetGetPreferencesAndEmptyWrite(
new ConfigObject(
array(
'location' => 'some/Path/To/Some/Directory'
)
),
Mockery::mock('Icinga\User', array('getUsername' => 'unittest'))
);
}
}

View File

@ -1,13 +1,13 @@
<?php <?php
/* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */ /* Icinga Web 2 | (c) 2014 Icinga Development Team | GPLv2+ */
namespace Tests\Icinga\User\Preferences\Store; namespace Tests\Icinga\User\Preferences;
use Icinga\User\Preferences\PreferencesStore;
use Mockery; use Mockery;
use Icinga\Data\ConfigObject; use Icinga\Data\ConfigObject;
use Icinga\Exception\NotWritableError; use Icinga\Exception\NotWritableError;
use Icinga\Test\BaseTestCase; use Icinga\Test\BaseTestCase;
use Icinga\User\Preferences\Store\DbStore;
class DatabaseMock class DatabaseMock
{ {
@ -22,19 +22,19 @@ class DatabaseMock
public function insert($table, $row) public function insert($table, $row)
{ {
$this->insertions[$row[DbStore::COLUMN_PREFERENCE]] = $row[DbStore::COLUMN_VALUE]; $this->insertions[$row[PreferencesStore::COLUMN_PREFERENCE]] = $row[PreferencesStore::COLUMN_VALUE];
} }
public function update($table, $columns, $where) public function update($table, $columns, $where)
{ {
$this->updates[$where[DbStore::COLUMN_PREFERENCE . '=?']] = $columns[DbStore::COLUMN_VALUE]; $this->updates[$where[PreferencesStore::COLUMN_PREFERENCE . '=?']] = $columns[PreferencesStore::COLUMN_VALUE];
} }
public function delete($table, $where) public function delete($table, $where)
{ {
$this->deletions = array_merge( $this->deletions = array_merge(
$this->deletions, $this->deletions,
$where[DbStore::COLUMN_PREFERENCE . ' IN (?)'] $where[PreferencesStore::COLUMN_PREFERENCE . ' IN (?)']
); );
} }
} }
@ -57,7 +57,7 @@ class FaultyDatabaseMock extends DatabaseMock
} }
} }
class DbStoreWithSetPreferences extends DbStore class PreferencesStoreWithSetPreferences extends PreferencesStore
{ {
public function setPreferences(array $preferences) public function setPreferences(array $preferences)
{ {
@ -65,7 +65,7 @@ class DbStoreWithSetPreferences extends DbStore
} }
} }
class DbStoreTest extends BaseTestCase class PreferencesStoreTest extends BaseTestCase
{ {
public function testWhetherPreferenceInsertionWorks() public function testWhetherPreferenceInsertionWorks()
{ {
@ -78,9 +78,9 @@ class DbStoreTest extends BaseTestCase
) )
); );
$this->assertArrayHasKey('key', $dbMock->insertions, 'DbStore::save does not insert new preferences'); $this->assertArrayHasKey('key', $dbMock->insertions, 'PreferencesStore::save does not insert new preferences');
$this->assertEmpty($dbMock->updates, 'DbStore::save updates *new* preferences'); $this->assertEmpty($dbMock->updates, 'PreferencesStore::save updates *new* preferences');
$this->assertEmpty($dbMock->deletions, 'DbStore::save deletes *new* preferences'); $this->assertEmpty($dbMock->deletions, 'PreferencesStore::save deletes *new* preferences');
} }
public function testWhetherPreferenceInsertionThrowsNotWritableError() public function testWhetherPreferenceInsertionThrowsNotWritableError()
@ -108,9 +108,9 @@ class DbStoreTest extends BaseTestCase
) )
); );
$this->assertArrayHasKey('key', $dbMock->updates, 'DbStore::save does not update existing preferences'); $this->assertArrayHasKey('key', $dbMock->updates, 'PreferencesStore::save does not update existing preferences');
$this->assertEmpty($dbMock->insertions, 'DbStore::save inserts *existing* preferences'); $this->assertEmpty($dbMock->insertions, 'PreferencesStore::save inserts *existing* preferences');
$this->assertEmpty($dbMock->deletions, 'DbStore::save inserts *existing* preferneces'); $this->assertEmpty($dbMock->deletions, 'PreferencesStore::save inserts *existing* preferneces');
} }
public function testWhetherPreferenceUpdatesThrowNotWritableError() public function testWhetherPreferenceUpdatesThrowNotWritableError()
@ -139,9 +139,9 @@ class DbStoreTest extends BaseTestCase
) )
); );
$this->assertContains('key', $dbMock->deletions, 'DbStore::save does not delete removed preferences'); $this->assertContains('key', $dbMock->deletions, 'PreferencesStore::save does not delete removed preferences');
$this->assertEmpty($dbMock->insertions, 'DbStore::save inserts *removed* preferences'); $this->assertEmpty($dbMock->insertions, 'PreferencesStore::save inserts *removed* preferences');
$this->assertEmpty($dbMock->updates, 'DbStore::save updates *removed* preferences'); $this->assertEmpty($dbMock->updates, 'PreferencesStore::save updates *removed* preferences');
} }
public function testWhetherPreferenceDeletionThrowsNotWritableError() public function testWhetherPreferenceDeletionThrowsNotWritableError()
@ -160,7 +160,7 @@ class DbStoreTest extends BaseTestCase
protected function getStore($dbMock) protected function getStore($dbMock)
{ {
return new DbStoreWithSetPreferences( return new PreferencesStoreWithSetPreferences(
new ConfigObject( new ConfigObject(
array( array(
'connection' => Mockery::mock(array('getDbAdapter' => $dbMock)) 'connection' => Mockery::mock(array('getDbAdapter' => $dbMock))