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->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(
'store' => $config->get('config_backend', 'db'),
'resource' => $config->config_resource
)), $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.
// this form is only displayed in configuration -> application if preferences backend type of ini is recognized
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
]
);
}
$backends = array_keys(ResourceFactory::getResourceConfigs()->toArray());
$backends = array_combine($backends, $backends);
if (! isset($formData['global_config_backend']) || $formData['global_config_backend'] === 'db') {
$backends = array();
foreach (ResourceFactory::getResourceConfigs()->toArray() as $name => $resource) {
if ($resource['type'] === 'db') {
$backends[$name] = $name;
}
}
$this->addElement(
'select',
'global_config_resource',
array(
'required' => true,
'multiOptions' => array_merge(
['' => sprintf(' - %s - ', $this->translate('Please choose'))],
$backends
),
'disable' => [''],
'value' => '',
'label' => $this->translate('Configuration Database')
)
);
}
$this->addElement(
'select',
'global_config_resource',
array(
'required' => true,
'multiOptions' => array_merge(
['' => sprintf(' - %s - ', $this->translate('Please choose'))],
$backends
),
'disable' => [''],
'value' => '',
'label' => $this->translate('Configuration Database')
)
);
return $this;
}

View File

@ -37,13 +37,4 @@ class GeneralConfigForm extends ConfigForm
$this->addSubForm($themingConfigForm->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`.
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 | **Optional.** Specify a defined [resource](04-Resources.md#resources-configuration-database) name. Can only be used if `config_backend` is set to `db`.
config\_resource | **Required.** Specify a defined [resource](04-Resources.md#resources-configuration-database) name.
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]
show_stacktraces = "0"
config_backend = "db"
config_resource = "icingaweb_db"
module_path = "/usr/share/icingaweb2/modules"
```

View File

@ -3,35 +3,13 @@
Preferences are settings a user can set for their account only,
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 in INI files beneath Icinga Web 2's configuration directory.
```
/etc/icingaweb2/<username>/config.ini
```
Preferences can be stored either in a MySQL or in a PostgreSQL database. The database must be configured.
## Configuration <a id="preferences-configuration"></a>
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>
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)
You have to define a [database resource](04-Resources.md#resources-configuration-database)
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
@ -39,6 +17,5 @@ in order to store preferences in a database.
```
[global]
config_backend = "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
* 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**

View File

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

View File

@ -3,18 +3,21 @@
namespace Icinga\User\Preferences;
use Icinga\Application\Config;
use Icinga\Application\Logger;
use Exception;
use Icinga\Exception\NotReadableError;
use Icinga\Exception\NotWritableError;
use Icinga\User;
use Icinga\User\Preferences;
use Icinga\Data\ConfigObject;
use Icinga\Data\ResourceFactory;
use Icinga\Exception\ConfigurationError;
use Icinga\Data\Db\DbConnection;
use Zend_Db_Expr;
/**
* Preferences store factory
*
* Load and save user preferences by using a database
*
* Usage example:
* <code>
* <?php
@ -23,11 +26,10 @@ use Icinga\Data\Db\DbConnection;
* use Icinga\User\Preferences;
* use Icinga\User\Preferences\PreferencesStore;
*
* // Create a INI store
* // Create a db store
* $store = PreferencesStore::create(
* new ConfigObject(
* 'store' => 'ini',
* 'config_path' => '/path/to/preferences'
* 'resource' => 'resource name'
* ),
* $user // Instance of \Icinga\User
* );
@ -37,8 +39,52 @@ use Icinga\Data\Db\DbConnection;
* $store->save($preferences);
* </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
*
@ -71,7 +117,7 @@ abstract class PreferencesStore
*
* @return ConfigObject
*/
public function getStoreConfig()
public function getStoreConfig(): ConfigObject
{
return $this->config;
}
@ -81,7 +127,7 @@ abstract class PreferencesStore
*
* @return User
*/
public function getUser()
public function getUser(): User
{
return $this->user;
}
@ -89,21 +135,190 @@ abstract class PreferencesStore
/**
* Initialize the store
*/
abstract protected function init();
protected function init(): void
{
}
/**
* Load preferences from source
* Load preferences from the database
*
* @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
*/
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
@ -115,29 +330,15 @@ abstract class PreferencesStore
*
* @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')));
$storeClass = 'Icinga\\User\\Preferences\\Store\\' . $type . 'Store';
if (!class_exists($storeClass)) {
throw new ConfigurationError(
'Preferences configuration defines an invalid storage type. Storage type %s not found',
$type
);
$resourceConfig = ResourceFactory::getResourceConfig($config->resource);
if ($resourceConfig->db === 'mysql') {
$resourceConfig->charset = 'utf8mb4';
}
if ($type === 'Ini') {
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);
$config->connection = ResourceFactory::createResource($resourceConfig);
}
return new $storeClass($config, $user);
return new self($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\Exception\NotReadableError;
use Icinga\Exception\NotWritableError;
use Icinga\File\Ini\IniParser;
use Icinga\User;
use Icinga\User\Preferences\Store\IniStore;
use Icinga\User\Preferences\Store\DbStore;
use Icinga\User\Preferences\PreferencesStore;
use Icinga\Util\DirectoryIterator;
class PreferencesCommand extends Command
@ -61,12 +61,15 @@ class PreferencesCommand extends Command
Logger::info('Migrating INI preferences for user "%s" to database...', $userName);
$iniStore = new IniStore(new ConfigObject(['location' => $preferencesPath]), new User($userName));
$dbStore = new DbStore(new ConfigObject(['connection' => $connection]), new User($userName));
$dbStore = new PreferencesStore(new ConfigObject(['connection' => $connection]), new User($userName));
try {
$dbStore->load();
$dbStore->save(new User\Preferences($iniStore->load()));
$dbStore->save(
new User\Preferences(
$this->loadIniFile($preferencesPath, (new User($userName))->getUsername())
)
);
} catch (NotReadableError $e) {
if ($e->getPrevious() !== null) {
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')) {
$appConfig = Config::app();
$globalConfig = $appConfig->getSection('global');
$globalConfig['config_backend'] = 'db';
$globalConfig['config_resource'] = $resource;
try {
@ -102,4 +104,28 @@ class PreferencesCommand extends Command
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();
$type = $config->get('global', 'config_backend', 'ini');
$resourceConfig = ResourceFactory::getResourceConfig($config->get('global', 'config_resource'));
if ($resourceConfig->db === 'mysql') {
$resourceConfig->charset = 'utf8mb4';
}
switch ($type) {
case 'ini':
$directory = Config::resolvePath('preferences');
/** @var DbConnection $conn */
$conn = ResourceFactory::createResource($resourceConfig);
$migration = array();
$query = $conn
->select()
->from('icingaweb_user_preference', array('username'))
->group('username');
if (DirectoryIterator::isReadable($directory)) {
foreach (new DirectoryIterator($directory) as $username => $path) {
$user = new User($username);
if ($this->map !== null) {
$query->applyFilter(Filter::matchAny(Filter::where('username', array_keys($this->map))));
}
if (! $this->mustMigrate($user)) {
continue;
}
$users = $query->fetchColumn();
$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) {
rename($from, $to);
}
}
if (! $this->mustMigrate($user)) {
continue;
}
break;
case 'db':
$resourceConfig = ResourceFactory::getResourceConfig($config->get('global', 'config_resource'));
if ($resourceConfig->db === 'mysql') {
$resourceConfig->charset = 'utf8mb4';
}
$migrated = $this->migrateUser($user);
/** @var DbConnection $conn */
$conn = ResourceFactory::createResource($resourceConfig);
$migration[$username] = $migrated->getUsername();
}
$query = $conn
->select()
->from('icingaweb_user_preference', array('username'))
->group('username');
if (! empty($migration)) {
$conn->getDbAdapter()->beginTransaction();
if ($this->map !== null) {
$query->applyFilter(Filter::matchAny(Filter::where('username', array_keys($this->map))));
}
foreach ($migration as $originalUsername => $username) {
$conn->update(
'icingaweb_user_preference',
array('username' => $username),
Filter::where('username', $originalUsername)
);
}
$users = $query->fetchColumn();
$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();
}
$conn->getDbAdapter()->commit();
}
}

View File

@ -28,9 +28,7 @@ class GeneralConfigStep extends Step
$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 {
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 hidden from every user by default.')
) . '</li>'
. '<li>' . sprintf(
$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>'
. '<li>' . t('Preferences will be stored using a database.') . '</li>'
. '</ul>';
$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
/* 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 Icinga\Data\ConfigObject;
use Icinga\Exception\NotWritableError;
use Icinga\Test\BaseTestCase;
use Icinga\User\Preferences\Store\DbStore;
class DatabaseMock
{
@ -22,19 +22,19 @@ class DatabaseMock
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)
{
$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)
{
$this->deletions = array_merge(
$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)
{
@ -65,7 +65,7 @@ class DbStoreWithSetPreferences extends DbStore
}
}
class DbStoreTest extends BaseTestCase
class PreferencesStoreTest extends BaseTestCase
{
public function testWhetherPreferenceInsertionWorks()
{
@ -78,9 +78,9 @@ class DbStoreTest extends BaseTestCase
)
);
$this->assertArrayHasKey('key', $dbMock->insertions, 'DbStore::save does not insert new preferences');
$this->assertEmpty($dbMock->updates, 'DbStore::save updates *new* preferences');
$this->assertEmpty($dbMock->deletions, 'DbStore::save deletes *new* preferences');
$this->assertArrayHasKey('key', $dbMock->insertions, 'PreferencesStore::save does not insert new preferences');
$this->assertEmpty($dbMock->updates, 'PreferencesStore::save updates *new* preferences');
$this->assertEmpty($dbMock->deletions, 'PreferencesStore::save deletes *new* preferences');
}
public function testWhetherPreferenceInsertionThrowsNotWritableError()
@ -108,9 +108,9 @@ class DbStoreTest extends BaseTestCase
)
);
$this->assertArrayHasKey('key', $dbMock->updates, 'DbStore::save does not update existing preferences');
$this->assertEmpty($dbMock->insertions, 'DbStore::save inserts *existing* preferences');
$this->assertEmpty($dbMock->deletions, 'DbStore::save inserts *existing* preferneces');
$this->assertArrayHasKey('key', $dbMock->updates, 'PreferencesStore::save does not update existing preferences');
$this->assertEmpty($dbMock->insertions, 'PreferencesStore::save inserts *existing* preferences');
$this->assertEmpty($dbMock->deletions, 'PreferencesStore::save inserts *existing* preferneces');
}
public function testWhetherPreferenceUpdatesThrowNotWritableError()
@ -139,9 +139,9 @@ class DbStoreTest extends BaseTestCase
)
);
$this->assertContains('key', $dbMock->deletions, 'DbStore::save does not delete removed preferences');
$this->assertEmpty($dbMock->insertions, 'DbStore::save inserts *removed* preferences');
$this->assertEmpty($dbMock->updates, 'DbStore::save updates *removed* preferences');
$this->assertContains('key', $dbMock->deletions, 'PreferencesStore::save does not delete removed preferences');
$this->assertEmpty($dbMock->insertions, 'PreferencesStore::save inserts *removed* preferences');
$this->assertEmpty($dbMock->updates, 'PreferencesStore::save updates *removed* preferences');
}
public function testWhetherPreferenceDeletionThrowsNotWritableError()
@ -160,7 +160,7 @@ class DbStoreTest extends BaseTestCase
protected function getStore($dbMock)
{
return new DbStoreWithSetPreferences(
return new PreferencesStoreWithSetPreferences(
new ConfigObject(
array(
'connection' => Mockery::mock(array('getDbAdapter' => $dbMock))