1<?php
2
3/**
4 +-----------------------------------------------------------------------+
5 | This file is part of the Roundcube Webmail client                     |
6 |                                                                       |
7 | Copyright (C) The Roundcube Dev Team                                  |
8 |                                                                       |
9 | Licensed under the GNU General Public License version 3 or            |
10 | any later version with exceptions for skins & plugins.                |
11 | See the README file for a full license statement.                     |
12 |                                                                       |
13 | PURPOSE:                                                              |
14 |   Roundcube Installer                                                 |
15 +-----------------------------------------------------------------------+
16 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
17 | Author: Aleksander Machniak <alec@alec.pl>                            |
18 +-----------------------------------------------------------------------+
19*/
20
21/**
22 * Class to control the installation process of the Roundcube Webmail package
23 *
24 * @category Install
25 * @package  Webmail
26 */
27class rcmail_install
28{
29    public $step;
30    public $last_error;
31    public $is_post           = false;
32    public $failures          = 0;
33    public $config            = [];
34    public $defaults          = [];
35    public $comments          = [];
36    public $configured        = false;
37    public $legacy_config     = false;
38    public $email_pattern     = '([a-z0-9][a-z0-9\-\.\+\_]*@[a-z0-9]([a-z0-9\-][.]?)*[a-z0-9])';
39    public $bool_config_props = [];
40
41    public $local_config    = ['db_dsnw', 'default_host', 'support_url', 'des_key', 'plugins'];
42    public $obsolete_config = ['db_backend', 'db_max_length', 'double_auth', 'preview_pane', 'debug_level', 'referer_check'];
43    public $replaced_config = [
44        'skin_path'            => 'skin',
45        'locale_string'        => 'language',
46        'multiple_identities'  => 'identities_level',
47        'addrbook_show_images' => 'show_images',
48        'imap_root'            => 'imap_ns_personal',
49        'pagesize'             => 'mail_pagesize',
50        'top_posting'          => 'reply_mode',
51        'keep_alive'           => 'refresh_interval',
52        'min_keep_alive'       => 'min_refresh_interval',
53    ];
54
55    // list of supported database drivers
56    public $supported_dbs = [
57        'MySQL'               => 'pdo_mysql',
58        'PostgreSQL'          => 'pdo_pgsql',
59        'SQLite'              => 'pdo_sqlite',
60        'SQLite (v2)'         => 'pdo_sqlite2',
61        'SQL Server (SQLSRV)' => 'pdo_sqlsrv',
62        'SQL Server (DBLIB)'  => 'pdo_dblib',
63        'Oracle'              => 'oci8',
64    ];
65
66    /** @var array List of config options with default value change per-release */
67    public $defaults_changes = [
68        '1.4.0' => ['skin', 'smtp_port', 'smtp_user', 'smtp_pass'],
69        '1.4.1' => ['jquery_ui_skin_map'],
70    ];
71
72    /**
73     * Constructor
74     */
75    public function __construct()
76    {
77        $this->step    = isset($_REQUEST['_step']) ? intval($_REQUEST['_step']) : 0;
78        $this->is_post = isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] == 'POST';
79    }
80
81    /**
82     * Singleton getter
83     */
84    public static function get_instance()
85    {
86        static $inst;
87
88        if (!$inst) {
89            $inst = new rcmail_install();
90        }
91
92        return $inst;
93    }
94
95    /**
96     * Read the local config files and store properties
97     */
98    public function load_config()
99    {
100        // defaults
101        if ($config = $this->load_config_file(RCUBE_CONFIG_DIR . 'defaults.inc.php')) {
102            $this->config   = (array) $config;
103            $this->defaults = $this->config;
104        }
105
106        $config = null;
107
108        // config
109        if ($config = $this->load_config_file(RCUBE_CONFIG_DIR . 'config.inc.php')) {
110            $this->config = array_merge($this->config, $config);
111        }
112        else {
113            if ($config = $this->load_config_file(RCUBE_CONFIG_DIR . 'main.inc.php')) {
114                $this->config        = array_merge($this->config, $config);
115                $this->legacy_config = true;
116            }
117
118            if ($config = $this->load_config_file(RCUBE_CONFIG_DIR . 'db.inc.php')) {
119                $this->config        = array_merge($this->config, $config);
120                $this->legacy_config = true;
121            }
122        }
123
124        $this->configured = !empty($config);
125    }
126
127    /**
128     * Read the default config file and store properties
129     *
130     * @param string $file File name with path
131     */
132    public function load_config_file($file)
133    {
134        if (!is_readable($file)) {
135            return;
136        }
137
138        $config        = [];
139        $rcmail_config = []; // deprecated var name
140
141        include $file;
142
143        // read comments from config file
144        if (function_exists('token_get_all')) {
145            $tokens    = token_get_all(file_get_contents($file));
146            $in_config = false;
147            $buffer    = '';
148
149            for ($i = 0; $i < count($tokens); $i++) {
150                $token = $tokens[$i];
151                if ($token[0] == T_VARIABLE && ($token[1] == '$config' || $token[1] == '$rcmail_config')) {
152                    $in_config = true;
153                    if ($buffer && $tokens[$i+1] == '[' && $tokens[$i+2][0] == T_CONSTANT_ENCAPSED_STRING) {
154                        $propname = trim($tokens[$i+2][1], "'\"");
155                        $this->comments[$propname] = $buffer;
156                        $buffer = '';
157                        $i += 3;
158                    }
159                }
160                else if ($in_config && $token[0] == T_COMMENT) {
161                    $buffer .= strtr($token[1], ['\n' => "\n"]) . "\n";
162                }
163            }
164        }
165
166        return array_merge((array) $rcmail_config, (array) $config);
167    }
168
169    /**
170     * Getter for a certain config property
171     *
172     * @param string $name    Property name
173     * @param string $default Default value
174     *
175     * @return mixed The property value
176     */
177    public function getprop($name, $default = '')
178    {
179        $value = isset($this->config[$name]) ? $this->config[$name] : null;
180
181        if ($name == 'des_key' && !$this->configured && !isset($_REQUEST["_$name"])) {
182            $value = rcube_utils::random_bytes(24);
183        }
184
185        return $value !== null && $value !== '' ? $value : $default;
186    }
187
188    /**
189     * Create configuration file that contains parameters
190     * that differ from default values.
191     *
192     * @return string The complete config file content
193     */
194    public function create_config()
195    {
196        $config = [];
197
198        foreach ($this->config as $prop => $default) {
199            $is_default = !isset($_POST["_$prop"]);
200            $value      = !$is_default || $this->bool_config_props[$prop] ? $_POST["_$prop"] : $default;
201
202            // always disable installer
203            if ($prop == 'enable_installer') {
204                $value = false;
205            }
206
207            // generate new encryption key, never use the default value
208            if ($prop == 'des_key' && $value == $this->defaults[$prop]) {
209                $value = rcube_utils::random_bytes(24);
210            }
211
212            // convert some form data
213            if ($prop == 'db_dsnw' && !empty($_POST['_dbtype'])) {
214                if ($_POST['_dbtype'] == 'sqlite') {
215                    $value = sprintf('%s://%s?mode=0646', $_POST['_dbtype'],
216                        $_POST['_dbname'][0] == '/' ? '/' . $_POST['_dbname'] : $_POST['_dbname']);
217                }
218                else if ($_POST['_dbtype']) {
219                    $value = sprintf('%s://%s:%s@%s/%s', $_POST['_dbtype'],
220                        rawurlencode($_POST['_dbuser']), rawurlencode($_POST['_dbpass']), $_POST['_dbhost'], $_POST['_dbname']);
221                }
222            }
223            else if ($prop == 'smtp_auth_type' && $value == '0') {
224                $value = '';
225            }
226            else if ($prop == 'default_host' && is_array($value)) {
227                $value = self::_clean_array($value);
228                if (count($value) <= 1) {
229                    $value = $value[0];
230                }
231            }
232            else if ($prop == 'mail_pagesize' || $prop == 'addressbook_pagesize') {
233                $value = max(2, intval($value));
234            }
235            else if ($prop == 'smtp_user' && !empty($_POST['_smtp_user_u'])) {
236                $value = '%u';
237            }
238            else if ($prop == 'smtp_pass' && !empty($_POST['_smtp_user_u'])) {
239                $value = '%p';
240            }
241            else if (is_bool($default)) {
242                $value = (bool) $value;
243            }
244            else if (is_numeric($value)) {
245                $value = intval($value);
246            }
247            else if ($prop == 'plugins' && !empty($_POST['submit'])) {
248                $value = [];
249                foreach (array_keys($_POST) as $key) {
250                    if (preg_match('/^_plugins_*/', $key)) {
251                        array_push($value, $_POST[$key]);
252                    }
253                }
254            }
255
256            // skip this property
257            if ($value == $this->defaults[$prop]
258                && (!in_array($prop, $this->local_config)
259                    || in_array($prop, array_merge($this->obsolete_config, array_keys($this->replaced_config)))
260                    || preg_match('/^db_(table|sequence)_/', $prop)
261                )
262            ) {
263                continue;
264            }
265
266            // save change
267            $this->config[$prop] = $value;
268            $config[$prop]       = $value;
269        }
270
271        $out = "<?php\n\n";
272        $out .= "/* Local configuration for Roundcube Webmail */\n\n";
273
274        foreach ($config as $prop => $value) {
275            // copy option descriptions from existing config or defaults.inc.php
276            $out .= $this->comments[$prop];
277            $out .= "\$config['$prop'] = " . self::_dump_var($value, $prop) . ";\n\n";
278        }
279
280        return $out;
281    }
282
283    /**
284     * save generated config file in RCUBE_CONFIG_DIR
285     *
286     * @return boolean True if the file was saved successfully, false if not
287     */
288    public function save_configfile($config)
289    {
290        if (is_writable(RCUBE_CONFIG_DIR)) {
291            return file_put_contents(RCUBE_CONFIG_DIR . 'config.inc.php', $config);
292        }
293
294        return false;
295    }
296
297    /**
298     * Check the current configuration for missing properties
299     * and deprecated or obsolete settings
300     *
301     * @param string $version Previous version on upgrade
302     *
303     * @return array List with problems detected
304     */
305    public function check_config($version = null)
306    {
307        $this->load_config();
308
309        if (!$this->configured) {
310            return;
311        }
312
313        $out = $seen = [];
314
315        // iterate over the current configuration
316        foreach (array_keys($this->config) as $prop) {
317            if (!empty($this->replaced_config[$prop])) {
318                $replacement = $this->replaced_config[$prop];
319                $out['replaced'][]  = ['prop' => $prop, 'replacement' => $replacement];
320                $seen[$replacement] = true;
321            }
322            else if (empty($seen[$prop]) && in_array($prop, $this->obsolete_config)) {
323                $out['obsolete'][] = ['prop' => $prop];
324                $seen[$prop]       = true;
325            }
326        }
327
328        // the old default mime_magic reference is obsolete
329        if ($this->config['mime_magic'] == '/usr/share/misc/magic') {
330            $out['obsolete'][] = [
331                'prop'    => 'mime_magic',
332                'explain' => "Set value to null in order to use system default"
333            ];
334        }
335
336        // check config dependencies and contradictions
337        if (!empty($this->config['enable_spellcheck']) && $this->config['spellcheck_engine'] == 'pspell') {
338            if (!extension_loaded('pspell')) {
339                $out['dependencies'][] = [
340                    'prop'    => 'spellcheck_engine',
341                    'explain' => "This requires the <tt>pspell</tt> extension which could not be loaded."
342                ];
343            }
344            else if (!empty($this->config['spellcheck_languages'])) {
345                foreach ($this->config['spellcheck_languages'] as $lang => $descr) {
346                    if (!@pspell_new($lang)) {
347                        $out['dependencies'][] = [
348                            'prop'    => 'spellcheck_languages',
349                            'explain' => "You are missing pspell support for language $lang ($descr)"
350                        ];
351                    }
352                }
353            }
354        }
355
356        if ($this->config['log_driver'] == 'syslog') {
357            if (!function_exists('openlog')) {
358                $out['dependencies'][] = [
359                    'prop'    => 'log_driver',
360                    'explain' => "This requires the <tt>syslog</tt> extension which could not be loaded."
361                ];
362            }
363
364            if (empty($this->config['syslog_id'])) {
365                $out['dependencies'][] = [
366                    'prop'    => 'syslog_id',
367                    'explain' => "Using <tt>syslog</tt> for logging requires a syslog ID to be configured"
368                ];
369            }
370        }
371
372        // check ldap_public sources having global_search enabled
373        if (is_array($this->config['ldap_public']) && !is_array($this->config['autocomplete_addressbooks'])) {
374            foreach ($this->config['ldap_public'] as $ldap_public) {
375                if ($ldap_public['global_search']) {
376                    $out['replaced'][] = [
377                        'prop'        => 'ldap_public::global_search',
378                        'replacement' => 'autocomplete_addressbooks'
379                    ];
380                    break;
381                }
382            }
383        }
384
385        if ($version) {
386            $out['defaults'] = [];
387
388            foreach ($this->defaults_changes as $v => $opts) {
389                if (version_compare($v, $version, '>')) {
390                    $out['defaults'] = array_merge($out['defaults'], $opts);
391                }
392            }
393
394            $out['defaults'] = array_unique($out['defaults']);
395        }
396
397        return $out;
398    }
399
400    /**
401     * Merge the current configuration with the defaults
402     * and copy replaced values to the new options.
403     */
404    public function merge_config()
405    {
406        $current      = $this->config;
407        $this->config = [];
408
409        foreach ($this->replaced_config as $prop => $replacement) {
410            if (isset($current[$prop])) {
411                if ($prop == 'skin_path') {
412                    $this->config[$replacement] = preg_replace('#skins/(\w+)/?$#', '\\1', $current[$prop]);
413                }
414                else if ($prop == 'multiple_identities') {
415                    $this->config[$replacement] = $current[$prop] ? 2 : 0;
416                }
417                else {
418                    $this->config[$replacement] = $current[$prop];
419                }
420            }
421
422            unset($current[$prop]);
423        }
424
425        foreach ($this->obsolete_config as $prop) {
426            unset($current[$prop]);
427        }
428
429        // add all ldap_public sources having global_search enabled to autocomplete_addressbooks
430        if (is_array($current['ldap_public'])) {
431            foreach ($current['ldap_public'] as $key => $ldap_public) {
432                if ($ldap_public['global_search']) {
433                    $this->config['autocomplete_addressbooks'][] = $key;
434                    unset($current['ldap_public'][$key]['global_search']);
435                }
436            }
437        }
438
439        $this->config = array_merge($this->config, $current);
440
441        foreach (array_keys((array) $current['ldap_public']) as $key) {
442            $this->config['ldap_public'][$key] = $current['ldap_public'][$key];
443        }
444    }
445
446    /**
447     * Compare the local database schema with the reference schema
448     * required for this version of Roundcube
449     *
450     * @param rcube_db $db Database object
451     *
452     * @return bool True if the schema is up-to-date, false if not or an error occurred
453     */
454    public function db_schema_check($db)
455    {
456        if (!$this->configured) {
457            return false;
458        }
459
460        // read reference schema from mysql.initial.sql
461        $engine    = $db->db_provider;
462        $db_schema = $this->db_read_schema(INSTALL_PATH . "SQL/$engine.initial.sql", $schema_version);
463        $errors    = [];
464
465        // Just check the version
466        if ($schema_version) {
467            $version = rcmail_utils::db_version();
468
469            if (empty($version)) {
470                $errors[] = "Schema version not found";
471            }
472            else if ($schema_version != $version) {
473                $errors[] = "Schema version: {$version} (required: {$schema_version})";
474            }
475
476            return !empty($errors) ? $errors : false;
477        }
478
479        // check list of tables
480        $existing_tables = $db->list_tables();
481
482        foreach ($db_schema as $table => $cols) {
483            $table = $this->config['db_prefix'] . $table;
484
485            if (!in_array($table, $existing_tables)) {
486                $errors[] = "Missing table '".$table."'";
487            }
488            else {  // compare cols
489                $db_cols = $db->list_cols($table);
490                $diff    = array_diff(array_keys($cols), $db_cols);
491
492                if (!empty($diff)) {
493                    $errors[] = "Missing columns in table '$table': " . implode(',', $diff);
494                }
495            }
496        }
497
498        return !empty($errors) ? $errors : false;
499    }
500
501    /**
502     * Utility function to read database schema from an .sql file
503     */
504    private function db_read_schema($schemafile, &$version = null)
505    {
506        $lines      = file($schemafile);
507        $schema     = [];
508        $keywords   = ['PRIMARY','KEY','INDEX','UNIQUE','CONSTRAINT','REFERENCES','FOREIGN'];
509        $table_name = null;
510
511        foreach ($lines as $line) {
512            if (preg_match('/^\s*create table ([\S]+)/i', $line, $m)) {
513                $table_name = explode('.', $m[1]);
514                $table_name = end($table_name);
515                $table_name = preg_replace('/[`"\[\]]/', '', $table_name);
516            }
517            else if (preg_match('/insert into/i', $line) && preg_match('/\'roundcube-version\',\s*\'([0-9]+)\'/', $line, $m)) {
518                $version = $m[1];
519            }
520            else if ($table_name && ($line = trim($line))) {
521                if ($line == 'GO' || $line[0] == ')' || $line[strlen($line)-1] == ';') {
522                    $table_name = null;
523                }
524                else {
525                    $items = explode(' ', $line);
526                    $col   = $items[0];
527                    $col   = preg_replace('/[`"\[\]]/', '', $col);
528
529                    if (!in_array(strtoupper($col), $keywords)) {
530                        $type = strtolower($items[1]);
531                        $type = preg_replace('/[^a-zA-Z0-9()]/', '', $type);
532
533                        $schema[$table_name][$col] = $type;
534                    }
535                }
536            }
537        }
538
539        return $schema;
540    }
541
542    /**
543     * Try to detect some file's mimetypes to test the correct behavior of fileinfo
544     */
545    public function check_mime_detection()
546    {
547        $errors = [];
548        $files  = [
549            'program/resources/tinymce/video.png' => 'image/png',
550            'program/resources/blank.tiff'        => 'image/tiff',
551            'program/resources/blocked.gif'       => 'image/gif',
552        ];
553
554        foreach ($files as $path => $expected) {
555            $mimetype = rcube_mime::file_content_type(INSTALL_PATH . $path, basename($path));
556            if ($mimetype != $expected) {
557                $errors[] = [$path, $mimetype, $expected];
558            }
559        }
560
561        return $errors;
562    }
563
564    /**
565     * Check the correct configuration of the 'mime_types' mapping option
566     */
567    public function check_mime_extensions()
568    {
569        $errors = [];
570        $types  = [
571            'application/zip'   => 'zip',
572            'text/css'          => 'css',
573            'application/pdf'   => 'pdf',
574            'image/gif'         => 'gif',
575            'image/svg+xml'     => 'svg',
576        ];
577
578        foreach ($types as $mimetype => $expected) {
579            $ext = rcube_mime::get_mime_extensions($mimetype);
580            if (!in_array($expected, (array) $ext)) {
581                $errors[] = [$mimetype, $ext, $expected];
582            }
583        }
584
585        return $errors;
586    }
587
588    /**
589     * Getter for the last error message
590     *
591     * @return string Error message or null if none exists
592     */
593    public function get_error()
594    {
595        return $this->last_error['message'];
596    }
597
598    /**
599     * Return a list with all imap/smtp hosts configured
600     *
601     * @return array Clean list with imap/smtp hosts
602     */
603    public function get_hostlist($prop = 'default_host')
604    {
605        $hosts     = (array) $this->getprop($prop);
606        $out       = [];
607        $imap_host = '';
608
609        if ($prop == 'smtp_server') {
610            // Set the imap host name for the %h macro
611            $default_hosts = $this->get_hostlist();
612            $imap_host = !empty($default_hosts) ? $default_hosts[0] : '';
613        }
614
615        foreach ($hosts as $key => $name) {
616            if (!empty($name)) {
617                if ($prop == 'smtp_server') {
618                    // SMTP host array uses `IMAP host => SMTP host` format
619                    $host = $name;
620                }
621                else {
622                    $host = is_numeric($key) ? $name : $key;
623                }
624
625                $out[] = rcube_utils::parse_host($host, $imap_host);
626            }
627        }
628
629        return $out;
630    }
631
632    /**
633     * Create a HTML dropdown to select a previous version of Roundcube
634     */
635    public function versions_select($attrib = [])
636    {
637        $select = new html_select($attrib);
638        $select->add([
639                '0.1-stable', '0.1.1',
640                '0.2-alpha', '0.2-beta', '0.2-stable',
641                '0.3-stable', '0.3.1',
642                '0.4-beta', '0.4.2',
643                '0.5-beta', '0.5', '0.5.1', '0.5.2', '0.5.3', '0.5.4',
644                '0.6-beta', '0.6',
645                '0.7-beta', '0.7', '0.7.1', '0.7.2', '0.7.3', '0.7.4',
646                '0.8-beta', '0.8-rc', '0.8.0', '0.8.1', '0.8.2', '0.8.3', '0.8.4', '0.8.5', '0.8.6',
647                '0.9-beta', '0.9-rc', '0.9-rc2',
648                // Note: Do not add newer versions here
649        ]);
650
651        return $select;
652    }
653
654    /**
655     * Return a list with available subfolders of the skin directory
656     *
657     * @return array List of available skins
658     */
659    public function list_skins()
660    {
661        $skins   = [];
662        $skindir = INSTALL_PATH . 'skins/';
663
664        foreach (glob($skindir . '*') as $path) {
665            if (is_dir($path) && is_readable($path)) {
666                $skins[] = substr($path, strlen($skindir));
667            }
668        }
669
670        return $skins;
671    }
672
673    /**
674     * Return a list with available subfolders of the plugins directory
675     * (with their associated description in composer.json)
676     *
677     * @return array List of available plugins
678     */
679    public function list_plugins()
680    {
681        $plugins    = [];
682        $plugin_dir = INSTALL_PATH . 'plugins/';
683        $enabled    = isset($this->config['plugins']) ? (array) $this->config['plugins'] : [];
684
685        foreach (glob($plugin_dir . '*') as $path) {
686            if (!is_dir($path)) {
687                continue;
688            }
689
690            if (is_readable($path . '/composer.json')) {
691                $file_json   = json_decode(file_get_contents($path . '/composer.json'));
692                $plugin_desc = $file_json->description ?: 'N/A';
693            }
694            else {
695                $plugin_desc = 'N/A';
696            }
697
698            $name      = substr($path, strlen($plugin_dir));
699            $plugins[] = [
700                'name'    => $name,
701                'desc'    => $plugin_desc,
702                'enabled' => in_array($name, $enabled)
703            ];
704        }
705
706        return $plugins;
707    }
708
709    /**
710     * Display OK status
711     *
712     * @param string $name    Test name
713     * @param string $message Confirm message
714     */
715    public function pass($name, $message = '')
716    {
717        echo rcube::Q($name) . ':&nbsp; <span class="success">OK</span>';
718        $this->_showhint($message);
719    }
720
721    /**
722     * Display an error status and increase failure count
723     *
724     * @param string $name     Test name
725     * @param string $message  Error message
726     * @param string $url      URL for details
727     * @param bool   $optional Do not count this failure
728     */
729    public function fail($name, $message = '', $url = '', $optional = false)
730    {
731        if (!$optional) {
732            $this->failures++;
733        }
734
735        echo rcube::Q($name) . ':&nbsp; <span class="fail">NOT OK</span>';
736        $this->_showhint($message, $url);
737    }
738
739    /**
740     * Display an error status for optional settings/features
741     *
742     * @param string $name    Test name
743     * @param string $message Error message
744     * @param string $url     URL for details
745     */
746    public function optfail($name, $message = '', $url = '')
747    {
748        echo rcube::Q($name) . ':&nbsp; <span class="na">NOT OK</span>';
749        $this->_showhint($message, $url);
750    }
751
752    /**
753     * Display warning status
754     *
755     * @param string $name    Test name
756     * @param string $message Warning message
757     * @param string $url     URL for details
758     */
759    public function na($name, $message = '', $url = '')
760    {
761        echo rcube::Q($name) . ':&nbsp; <span class="na">NOT AVAILABLE</span>';
762        $this->_showhint($message, $url);
763    }
764
765    private function _showhint($message, $url = '')
766    {
767        $hint = rcube::Q($message);
768
769        if ($url) {
770            $hint .= ($hint ? '; ' : '') . 'See <a href="' . rcube::Q($url) . '" target="_blank">' . rcube::Q($url) . '</a>';
771        }
772
773        if ($hint) {
774            echo '<span class="indent">(' . $hint . ')</span>';
775        }
776    }
777
778    private static function _clean_array($arr)
779    {
780        $out = [];
781
782        foreach (array_unique($arr) as $k => $val) {
783            if (!empty($val)) {
784                if (is_numeric($k)) {
785                    $out[] = $val;
786                }
787                else {
788                    $out[$k] = $val;
789                }
790            }
791        }
792
793        return $out;
794    }
795
796    private static function _dump_var($var, $name = null)
797    {
798        // special values
799        switch ($name) {
800        case 'syslog_facility':
801            $list = [
802                32 => 'LOG_AUTH', 80 => 'LOG_AUTHPRIV', 72 => ' LOG_CRON',
803                24 => 'LOG_DAEMON', 0 => 'LOG_KERN', 128 => 'LOG_LOCAL0',
804                136 => 'LOG_LOCAL1', 144 => 'LOG_LOCAL2', 152 => 'LOG_LOCAL3',
805                160 => 'LOG_LOCAL4', 168 => 'LOG_LOCAL5', 176 => 'LOG_LOCAL6',
806                184 => 'LOG_LOCAL7', 48 => 'LOG_LPR', 16 => 'LOG_MAIL',
807                56 => 'LOG_NEWS', 40 => 'LOG_SYSLOG', 8 => 'LOG_USER', 64 => 'LOG_UUCP'
808            ];
809
810            if (!empty($list[$var])) {
811                return $list[$var];
812            }
813
814            break;
815        }
816
817        if (is_array($var)) {
818            if (empty($var)) {
819                return '[]';
820            }
821            // check if all keys are numeric
822            $isnum = true;
823            foreach (array_keys($var) as $key) {
824                if (!is_numeric($key)) {
825                    $isnum = false;
826                    break;
827                }
828            }
829
830            if ($isnum) {
831                return '[' . implode(', ', array_map(['rcmail_install', '_dump_var'], $var)) . ']';
832            }
833        }
834
835        return var_export($var, true);
836    }
837
838    /**
839     * Initialize the database with the according schema
840     *
841     * @param rcube_db $db Database connection
842     *
843     * @return bool True on success, False on error
844     */
845    public function init_db($db)
846    {
847        $engine = $db->db_provider;
848
849        // read schema file from /SQL/*
850        $fname = INSTALL_PATH . "SQL/$engine.initial.sql";
851        if ($sql = @file_get_contents($fname)) {
852            $db->set_option('table_prefix', $this->config['db_prefix']);
853            $db->exec_script($sql);
854        }
855        else {
856            $this->fail('DB Schema', "Cannot read the schema file: $fname");
857            return false;
858        }
859
860        if ($err = $this->get_error()) {
861            $this->fail('DB Schema', "Error creating database schema: $err");
862            return false;
863        }
864
865        return true;
866    }
867
868    /**
869     * Update database schema
870     *
871     * @param string $version Version to update from
872     *
873     * @return boolean True on success, False on error
874     */
875    public function update_db($version)
876    {
877        return rcmail_utils::db_update(INSTALL_PATH . 'SQL', 'roundcube', $version, ['quiet' => true]);
878    }
879
880    /**
881     * Handler for Roundcube errors
882     */
883    public function raise_error($p)
884    {
885        $this->last_error = $p;
886    }
887}
888