+
+
+ /**
+ * Check the current configuration for missing properties
+ * and deprecated or obsolete settings
+ *
+ * @return array List with problems detected
+ */
+ function check_config()
+ {
+ $this->config = array();
+ $this->load_defaults();
+ $defaults = $this->config;
+
+ $this->load_config();
+ if (!$this->configured)
+ return null;
+
+ $out = $seen = array();
+ $required = array_flip($this->required_config);
+
+ // iterate over the current configuration
+ foreach ($this->config as $prop => $value) {
+ if ($replacement = $this->replaced_config[$prop]) {
+ $out['replaced'][] = array('prop' => $prop, 'replacement' => $replacement);
+ $seen[$replacement] = true;
+ }
+ else if (!$seen[$prop] && in_array($prop, $this->obsolete_config)) {
+ $out['obsolete'][] = array('prop' => $prop);
+ $seen[$prop] = true;
+ }
+ }
+
+ // iterate over default config
+ foreach ($defaults as $prop => $value) {
+ if (!isset($seen[$prop]) && !isset($this->config[$prop]) && isset($required[$prop]))
+ $out['missing'][] = array('prop' => $prop);
+ }
+
+ // check config dependencies and contradictions
+ if ($this->config['enable_spellcheck'] && $this->config['spellcheck_engine'] == 'pspell') {
+ if (!extension_loaded('pspell')) {
+ $out['dependencies'][] = array('prop' => 'spellcheck_engine',
+ 'explain' => 'This requires the <tt>pspell</tt> extension which could not be loaded.');
+ }
+ else if (!empty($this->config['spellcheck_languages'])) {
+ foreach ($this->config['spellcheck_languages'] as $lang => $descr)
+ if (!pspell_new($lang))
+ $out['dependencies'][] = array('prop' => 'spellcheck_languages',
+ 'explain' => "You are missing pspell support for language $lang ($descr)");
+ }
+ }
+
+ if ($this->config['log_driver'] == 'syslog') {
+ if (!function_exists('openlog')) {
+ $out['dependencies'][] = array('prop' => 'log_driver',
+ 'explain' => 'This requires the <tt>sylog</tt> extension which could not be loaded.');
+ }
+ if (empty($this->config['syslog_id'])) {
+ $out['dependencies'][] = array('prop' => 'syslog_id',
+ 'explain' => 'Using <tt>syslog</tt> for logging requires a syslog ID to be configured');
+ }
+ }
+
+ // check ldap_public sources having global_search enabled
+ if (is_array($this->config['ldap_public']) && !is_array($this->config['autocomplete_addressbooks'])) {
+ foreach ($this->config['ldap_public'] as $ldap_public) {
+ if ($ldap_public['global_search']) {
+ $out['replaced'][] = array('prop' => 'ldap_public::global_search', 'replacement' => 'autocomplete_addressbooks');
+ break;
+ }
+ }
+ }
+
+ return $out;
+ }
+
+
+ /**
+ * Merge the current configuration with the defaults
+ * and copy replaced values to the new options.
+ */
+ function merge_config()
+ {
+ $current = $this->config;
+ $this->config = array();
+ $this->load_defaults();
+
+ foreach ($this->replaced_config as $prop => $replacement) {
+ if (isset($current[$prop])) {
+ if ($prop == 'skin_path')
+ $this->config[$replacement] = preg_replace('#skins/(\w+)/?$#', '\\1', $current[$prop]);
+ else if ($prop == 'multiple_identities')
+ $this->config[$replacement] = $current[$prop] ? 2 : 0;
+ else
+ $this->config[$replacement] = $current[$prop];
+ }
+ unset($current[$prop]);
+ }
+
+ foreach ($this->obsolete_config as $prop) {
+ unset($current[$prop]);
+ }
+
+ // add all ldap_public sources having global_search enabled to autocomplete_addressbooks
+ if (is_array($current['ldap_public'])) {
+ foreach ($current['ldap_public'] as $key => $ldap_public) {
+ if ($ldap_public['global_search']) {
+ $this->config['autocomplete_addressbooks'][] = $key;
+ unset($current['ldap_public'][$key]['global_search']);
+ }
+ }
+ }
+
+ if ($current['keep_alive'] && $current['session_lifetime'] < $current['keep_alive'])
+ $current['session_lifetime'] = max(10, ceil($current['keep_alive'] / 60) * 2);
+
+ $this->config = array_merge($this->config, $current);
+
+ foreach ((array)$current['ldap_public'] as $key => $values) {
+ $this->config['ldap_public'][$key] = $current['ldap_public'][$key];
+ }
+ }
+
+ /**
+ * Compare the local database schema with the reference schema
+ * required for this version of Roundcube
+ *
+ * @param boolean True if the schema schould be updated
+ * @return boolean True if the schema is up-to-date, false if not or an error occured
+ */
+ function db_schema_check($DB, $update = false)
+ {
+ if (!$this->configured)
+ return false;
+
+ // read reference schema from mysql.initial.sql
+ $db_schema = $this->db_read_schema(INSTALL_PATH . 'SQL/mysql.initial.sql');
+ $errors = array();
+
+ // check list of tables
+ $existing_tables = $DB->list_tables();
+
+ foreach ($db_schema as $table => $cols) {
+ $table = !empty($this->config['db_table_'.$table]) ? $this->config['db_table_'.$table] : $table;
+ if (!in_array($table, $existing_tables)) {
+ $errors[] = "Missing table '".$table."'";
+ }
+ else { // compare cols
+ $db_cols = $DB->list_cols($table);
+ $diff = array_diff(array_keys($cols), $db_cols);
+ if (!empty($diff))
+ $errors[] = "Missing columns in table '$table': " . join(',', $diff);
+ }
+ }
+
+ return !empty($errors) ? $errors : false;
+ }
+
+ /**
+ * Utility function to read database schema from an .sql file
+ */
+ private function db_read_schema($schemafile)
+ {
+ $lines = file($schemafile);
+ $table_block = false;
+ $schema = array();
+ foreach ($lines as $line) {
+ if (preg_match('/^\s*create table `?([a-z0-9_]+)`?/i', $line, $m)) {
+ $table_block = $m[1];
+ }
+ else if ($table_block && preg_match('/^\s*`?([a-z0-9_-]+)`?\s+([a-z]+)/', $line, $m)) {
+ $col = $m[1];
+ if (!in_array(strtoupper($col), array('PRIMARY','KEY','INDEX','UNIQUE','CONSTRAINT','REFERENCES','FOREIGN'))) {
+ $schema[$table_block][$col] = $m[2];
+ }
+ }
+ }
+
+ return $schema;
+ }
+
+
+ /**
+ * Compare the local database schema with the reference schema
+ * required for this version of Roundcube
+ *
+ * @param boolean True if the schema schould be updated
+ * @return boolean True if the schema is up-to-date, false if not or an error occured
+ */
+ function mdb2_schema_check($update = false)
+ {
+ if (!$this->configured)
+ return false;
+
+ $options = array(
+ 'use_transactions' => false,
+ 'log_line_break' => "\n",
+ 'idxname_format' => '%s',
+ 'debug' => false,
+ 'quote_identifier' => true,
+ 'force_defaults' => false,
+ 'portability' => true
+ );
+
+ $dsnw = $this->config['db_dsnw'];
+ $schema = MDB2_Schema::factory($dsnw, $options);
+ $schema->db->supported['transactions'] = false;
+
+ if (PEAR::isError($schema)) {
+ $this->raise_error(array('code' => $schema->getCode(), 'message' => $schema->getMessage() . ' ' . $schema->getUserInfo()));
+ return false;
+ }
+ else {
+ $definition = $schema->getDefinitionFromDatabase();
+ $definition['charset'] = 'utf8';
+
+ if (PEAR::isError($definition)) {
+ $this->raise_error(array('code' => $definition->getCode(), 'message' => $definition->getMessage() . ' ' . $definition->getUserInfo()));
+ return false;
+ }
+
+ // load reference schema
+ $dsn_arr = MDB2::parseDSN($this->config['db_dsnw']);
+
+ $ref_schema = INSTALL_PATH . 'SQL/' . $dsn_arr['phptype'] . '.schema.xml';
+
+ if (is_readable($ref_schema)) {
+ $reference = $schema->parseDatabaseDefinition($ref_schema, false, array(), $schema->options['fail_on_invalid_names']);
+
+ if (PEAR::isError($reference)) {
+ $this->raise_error(array('code' => $reference->getCode(), 'message' => $reference->getMessage() . ' ' . $reference->getUserInfo()));
+ }
+ else {
+ $diff = $schema->compareDefinitions($reference, $definition);
+
+ if (empty($diff)) {
+ return true;
+ }
+ else if ($update) {
+ // update database schema with the diff from the above check
+ $success = $schema->alterDatabase($reference, $definition, $diff);
+
+ if (PEAR::isError($success)) {
+ $this->raise_error(array('code' => $success->getCode(), 'message' => $success->getMessage() . ' ' . $success->getUserInfo()));
+ }
+ else
+ return true;
+ }
+ echo '<pre>'; var_dump($diff); echo '</pre>';
+ return false;
+ }
+ }
+ else
+ $this->raise_error(array('message' => "Could not find reference schema file ($ref_schema)"));
+ return false;
+ }
+
+ return false;
+ }