diff --git a/application/controllers/AccountController.php b/application/controllers/AccountController.php index 4dc655452..f172cfeca 100644 --- a/application/controllers/AccountController.php +++ b/application/controllers/AccountController.php @@ -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)); } diff --git a/application/forms/Config/General/ApplicationConfigForm.php b/application/forms/Config/General/ApplicationConfigForm.php index 63a32fa9b..0e5c70028 100644 --- a/application/forms/Config/General/ApplicationConfigForm.php +++ b/application/forms/Config/General/ApplicationConfigForm.php @@ -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; } diff --git a/application/forms/Config/GeneralConfigForm.php b/application/forms/Config/GeneralConfigForm.php index 18d379094..5f15512a5 100644 --- a/application/forms/Config/GeneralConfigForm.php +++ b/application/forms/Config/GeneralConfigForm.php @@ -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'); - } - } } diff --git a/doc/03-Configuration.md b/doc/03-Configuration.md index 56f736030..a85416585 100644 --- a/doc/03-Configuration.md +++ b/doc/03-Configuration.md @@ -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" ``` diff --git a/doc/07-Preferences.md b/doc/07-Preferences.md index 341e41ffb..73abead35 100644 --- a/doc/07-Preferences.md +++ b/doc/07-Preferences.md @@ -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//config.ini -``` +Preferences can be stored either in a MySQL or in a PostgreSQL database. The database must be configured. ## Configuration The preference configuration backend is defined in the global [config.ini](03-Configuration.md#configuration-general-global) file. -### Store Preferences in INI Files - -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 - -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" ``` diff --git a/doc/80-Upgrading.md b/doc/80-Upgrading.md index a24d4ac4c..ca7490094 100644 --- a/doc/80-Upgrading.md +++ b/doc/80-Upgrading.md @@ -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** diff --git a/library/Icinga/Authentication/Auth.php b/library/Icinga/Authentication/Auth.php index c02f75084..f358eac37 100644 --- a/library/Icinga/Authentication/Auth.php +++ b/library/Icinga/Authentication/Auth.php @@ -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(); } diff --git a/library/Icinga/User/Preferences/PreferencesStore.php b/library/Icinga/User/Preferences/PreferencesStore.php index d3c5f5397..8ecc677c9 100644 --- a/library/Icinga/User/Preferences/PreferencesStore.php +++ b/library/Icinga/User/Preferences/PreferencesStore.php @@ -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: * * '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); * */ -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); } } diff --git a/library/Icinga/User/Preferences/Store/DbStore.php b/library/Icinga/User/Preferences/Store/DbStore.php deleted file mode 100644 index 341a98358..000000000 --- a/library/Icinga/User/Preferences/Store/DbStore.php +++ /dev/null @@ -1,255 +0,0 @@ -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 - ); - } - } -} diff --git a/library/Icinga/User/Preferences/Store/IniStore.php b/library/Icinga/User/Preferences/Store/IniStore.php deleted file mode 100644 index f456f141a..000000000 --- a/library/Icinga/User/Preferences/Store/IniStore.php +++ /dev/null @@ -1,118 +0,0 @@ -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]); - } - } -} diff --git a/modules/migrate/application/clicommands/PreferencesCommand.php b/modules/migrate/application/clicommands/PreferencesCommand.php index f31665493..11d1edb94 100644 --- a/modules/migrate/application/clicommands/PreferencesCommand.php +++ b/modules/migrate/application/clicommands/PreferencesCommand.php @@ -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; + } } diff --git a/modules/migrate/library/Migrate/Config/UserDomainMigration.php b/modules/migrate/library/Migrate/Config/UserDomainMigration.php index 1c3f6e181..855a0abdb 100644 --- a/modules/migrate/library/Migrate/Config/UserDomainMigration.php +++ b/modules/migrate/library/Migrate/Config/UserDomainMigration.php @@ -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(); } } diff --git a/modules/setup/library/Setup/Steps/GeneralConfigStep.php b/modules/setup/library/Setup/Steps/GeneralConfigStep.php index f15ba6e52..2c928f63b 100644 --- a/modules/setup/library/Setup/Steps/GeneralConfigStep.php +++ b/modules/setup/library/Setup/Steps/GeneralConfigStep.php @@ -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.') ) . '' - . '
  • ' . 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.') - ) . '
  • ' + . '
  • ' . t('Preferences will be stored using a database.') . '
  • ' . ''; $type = $this->data['generalConfig']['logging_log']; diff --git a/test/php/library/Icinga/User/Store/IniStoreTest.php b/test/php/library/Icinga/User/Store/IniStoreTest.php deleted file mode 100644 index c5baef9c4..000000000 --- a/test/php/library/Icinga/User/Store/IniStoreTest.php +++ /dev/null @@ -1,71 +0,0 @@ -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')) - ); - } -} diff --git a/test/php/library/Icinga/User/Store/DbStoreTest.php b/test/php/library/Icinga/User/Store/PreferencesStoreTest.php similarity index 72% rename from test/php/library/Icinga/User/Store/DbStoreTest.php rename to test/php/library/Icinga/User/Store/PreferencesStoreTest.php index 1f56f93cb..4ae8d712f 100644 --- a/test/php/library/Icinga/User/Store/DbStoreTest.php +++ b/test/php/library/Icinga/User/Store/PreferencesStoreTest.php @@ -1,13 +1,13 @@ 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))