source: subversion/trunk/roundcubemail/installer/rcube_install.php @ 3989

Last change on this file since 3989 was 3989, checked in by alec, 3 years ago
  • s/RoundCube/Roundcube/
File size: 18.2 KB
Line 
1<?php
2
3/*
4 +-----------------------------------------------------------------------+
5 | rcube_install.php                                                     |
6 |                                                                       |
7 | This file is part of the Roundcube Webmail package                    |
8 | Copyright (C) 2008-2009, Roundcube Dev. - Switzerland                 |
9 | Licensed under the GNU Public License                                 |
10 +-----------------------------------------------------------------------+
11
12 $Id:  $
13
14*/
15
16
17/**
18 * Class to control the installation process of the Roundcube Webmail package
19 *
20 * @category Install
21 * @package  Roundcube
22 * @author Thomas Bruederli
23 */
24class rcube_install
25{
26  var $step;
27  var $is_post = false;
28  var $failures = 0;
29  var $config = array();
30  var $configured = false;
31  var $last_error = null;
32  var $email_pattern = '([a-z0-9][a-z0-9\-\.\+\_]*@[a-z0-9]([a-z0-9\-][.]?)*[a-z0-9])';
33  var $bool_config_props = array();
34
35  var $obsolete_config = array('db_backend');
36  var $replaced_config = array(
37    'skin_path' => 'skin',
38    'locale_string' => 'language',
39    'multiple_identities' => 'identities_level',
40    'addrbook_show_images' => 'show_images',
41  );
42 
43  // these config options are required for a working system
44  var $required_config = array('db_dsnw', 'db_table_contactgroups', 'db_table_contactgroupmembers', 'des_key');
45 
46  /**
47   * Constructor
48   */
49  function rcube_install()
50  {
51    $this->step = intval($_REQUEST['_step']);
52    $this->is_post = $_SERVER['REQUEST_METHOD'] == 'POST';
53  }
54 
55  /**
56   * Singleton getter
57   */
58  function get_instance()
59  {
60    static $inst;
61   
62    if (!$inst)
63      $inst = new rcube_install();
64   
65    return $inst;
66  }
67 
68  /**
69   * Read the default config files and store properties
70   */
71  function load_defaults()
72  {
73    $this->_load_config('.php.dist');
74  }
75
76
77  /**
78   * Read the local config files and store properties
79   */
80  function load_config()
81  {
82    $this->config = array();
83    $this->_load_config('.php');
84    $this->configured = !empty($this->config);
85  }
86
87  /**
88   * Read the default config file and store properties
89   * @access private
90   */
91  function _load_config($suffix)
92  {
93    @include RCMAIL_CONFIG_DIR . '/main.inc' . $suffix;
94    if (is_array($rcmail_config)) {
95      $this->config += $rcmail_config;
96    }
97     
98    @include RCMAIL_CONFIG_DIR . '/db.inc'. $suffix;
99    if (is_array($rcmail_config)) {
100      $this->config += $rcmail_config;
101    }
102  }
103 
104 
105  /**
106   * Getter for a certain config property
107   *
108   * @param string Property name
109   * @param string Default value
110   * @return string The property value
111   */
112  function getprop($name, $default = '')
113  {
114    $value = $this->config[$name];
115   
116    if ($name == 'des_key' && !$this->configured && !isset($_REQUEST["_$name"]))
117      $value = rcube_install::random_key(24);
118   
119    return $value !== null && $value !== '' ? $value : $default;
120  }
121 
122 
123  /**
124   * Take the default config file and replace the parameters
125   * with the submitted form data
126   *
127   * @param string Which config file (either 'main' or 'db')
128   * @return string The complete config file content
129   */
130  function create_config($which, $force = false)
131  {
132    $out = @file_get_contents(RCMAIL_CONFIG_DIR . "/{$which}.inc.php.dist");
133   
134    if (!$out)
135      return '[Warning: could not read the config template file]';
136
137    foreach ($this->config as $prop => $default) {
138      $value = (isset($_POST["_$prop"]) || $this->bool_config_props[$prop]) ? $_POST["_$prop"] : $default;
139     
140      // convert some form data
141      if ($prop == 'debug_level') {
142        $val = 0;
143        if (is_array($value))
144          foreach ($value as $dbgval)
145            $val += intval($dbgval);
146        $value = $val;
147      }
148      else if ($which == 'db' && $prop == 'db_dsnw' && !empty($_POST['_dbtype'])) {
149        if ($_POST['_dbtype'] == 'sqlite')
150          $value = sprintf('%s://%s?mode=0646', $_POST['_dbtype'], $_POST['_dbname']{0} == '/' ? '/' . $_POST['_dbname'] : $_POST['_dbname']);
151        else
152          $value = sprintf('%s://%s:%s@%s/%s', $_POST['_dbtype'], 
153            rawurlencode($_POST['_dbuser']), rawurlencode($_POST['_dbpass']), $_POST['_dbhost'], $_POST['_dbname']);
154      }
155      else if ($prop == 'smtp_auth_type' && $value == '0') {
156        $value = '';
157      }
158      else if ($prop == 'default_host' && is_array($value)) {
159        $value = rcube_install::_clean_array($value);
160        if (count($value) <= 1)
161          $value = $value[0];
162      }
163      else if ($prop == 'pagesize') {
164        $value = max(2, intval($value));
165      }
166      else if ($prop == 'smtp_user' && !empty($_POST['_smtp_user_u'])) {
167        $value = '%u';
168      }
169      else if ($prop == 'smtp_pass' && !empty($_POST['_smtp_user_u'])) {
170        $value = '%p';
171      }
172      else if ($prop == 'default_imap_folders'){
173        $value = Array();
174        foreach($this->config['default_imap_folders'] as $_folder){
175          switch($_folder) {
176          case 'Drafts': $_folder = $this->config['drafts_mbox']; break;
177          case 'Sent':   $_folder = $this->config['sent_mbox']; break;
178          case 'Junk':   $_folder = $this->config['junk_mbox']; break;
179          case 'Trash':  $_folder = $this->config['trash_mbox']; break;
180          }
181          if (!in_array($_folder, $value))  $value[] = $_folder;
182        }
183      }
184      else if (is_bool($default)) {
185        $value = (bool)$value;
186      }
187      else if (is_numeric($value)) {
188        $value = intval($value);
189      }
190     
191      // skip this property
192      if (!$force && ($value == $default))
193        continue;
194
195      // save change
196      $this->config[$prop] = $value;
197
198      // replace the matching line in config file
199      $out = preg_replace(
200        '/(\$rcmail_config\[\''.preg_quote($prop).'\'\])\s+=\s+(.+);/Uie',
201        "'\\1 = ' . rcube_install::_dump_var(\$value) . ';'",
202        $out);
203    }
204
205    return trim($out);
206  }
207
208
209  /**
210   * Check the current configuration for missing properties
211   * and deprecated or obsolete settings
212   *
213   * @return array List with problems detected
214   */
215  function check_config()
216  {
217    $this->config = array();
218    $this->load_defaults();
219    $defaults = $this->config;
220   
221    $this->load_config();
222    if (!$this->configured)
223      return null;
224   
225    $out = $seen = array();
226    $required = array_flip($this->required_config);
227   
228    // iterate over the current configuration
229    foreach ($this->config as $prop => $value) {
230      if ($replacement = $this->replaced_config[$prop]) {
231        $out['replaced'][] = array('prop' => $prop, 'replacement' => $replacement);
232        $seen[$replacement] = true;
233      }
234      else if (!$seen[$prop] && in_array($prop, $this->obsolete_config)) {
235        $out['obsolete'][] = array('prop' => $prop);
236        $seen[$prop] = true;
237      }
238    }
239   
240    // iterate over default config
241    foreach ($defaults as $prop => $value) {
242      if (!isset($seen[$prop]) && !isset($this->config[$prop]) && isset($required[$prop]))
243        $out['missing'][] = array('prop' => $prop);
244    }
245
246    // check config dependencies and contradictions
247    if ($this->config['enable_spellcheck'] && $this->config['spellcheck_engine'] == 'pspell') {
248      if (!extension_loaded('pspell')) {
249        $out['dependencies'][] = array('prop' => 'spellcheck_engine',
250          'explain' => 'This requires the <tt>pspell</tt> extension which could not be loaded.');
251      }
252      else if (!empty($this->config['spellcheck_languages'])) {
253        foreach ($this->config['spellcheck_languages'] as $lang => $descr)
254          if (!pspell_new($lang))
255            $out['dependencies'][] = array('prop' => 'spellcheck_languages',
256              'explain' => "You are missing pspell support for language $lang ($descr)");
257      }
258    }
259   
260    if ($this->config['log_driver'] == 'syslog') {
261      if (!function_exists('openlog')) {
262        $out['dependencies'][] = array('prop' => 'log_driver',
263          'explain' => 'This requires the <tt>sylog</tt> extension which could not be loaded.');
264      }
265      if (empty($this->config['syslog_id'])) {
266        $out['dependencies'][] = array('prop' => 'syslog_id',
267          'explain' => 'Using <tt>syslog</tt> for logging requires a syslog ID to be configured');
268      }
269    }
270   
271    // check ldap_public sources having global_search enabled
272    if (is_array($this->config['ldap_public']) && !is_array($this->config['autocomplete_addressbooks'])) {
273      foreach ($this->config['ldap_public'] as $ldap_public) {
274        if ($ldap_public['global_search']) {
275          $out['replaced'][] = array('prop' => 'ldap_public::global_search', 'replacement' => 'autocomplete_addressbooks');
276          break;
277        }
278      }
279    }
280   
281    return $out;
282  }
283 
284 
285  /**
286   * Merge the current configuration with the defaults
287   * and copy replaced values to the new options.
288   */
289  function merge_config()
290  {
291    $current = $this->config;
292    $this->config = array();
293    $this->load_defaults();
294   
295    foreach ($this->replaced_config as $prop => $replacement)
296      if (isset($current[$prop])) {
297        if ($prop == 'skin_path')
298          $this->config[$replacement] = preg_replace('#skins/(\w+)/?$#', '\\1', $current[$prop]);
299        else if ($prop == 'multiple_identities')
300          $this->config[$replacement] = $current[$prop] ? 2 : 0;
301        else
302          $this->config[$replacement] = $current[$prop];
303       
304        unset($current[$prop]);
305    }
306   
307    foreach ($this->obsolete_config as $prop) {
308      unset($current[$prop]);
309    }
310   
311    // add all ldap_public sources having global_search enabled to autocomplete_addressbooks
312    if (is_array($current['ldap_public'])) {
313      foreach ($current['ldap_public'] as $key => $ldap_public) {
314        if ($ldap_public['global_search']) {
315          $this->config['autocomplete_addressbooks'][] = $key;
316          unset($current['ldap_public'][$key]['global_search']);
317        }
318      }
319    }
320   
321    $this->config  = array_merge($this->config, $current);
322   
323    foreach ((array)$current['ldap_public'] as $key => $values) {
324      $this->config['ldap_public'][$key] = $current['ldap_public'][$key];
325    }
326  }
327 
328  /**
329   * Compare the local database schema with the reference schema
330   * required for this version of Roundcube
331   *
332   * @param boolean True if the schema schould be updated
333   * @return boolean True if the schema is up-to-date, false if not or an error occured
334   */
335  function db_schema_check($DB, $update = false)
336  {
337    if (!$this->configured)
338      return false;
339   
340    // simple ad hand-made db schema
341    $db_schema = array(
342      'users' => array(),
343      'identities' => array(),
344      'contacts' => array(),
345      'contactgroups' => array(),
346      'contactgroupmembers' => array(),
347      'cache' => array(),
348      'messages' => array(),
349      'session' => array(),
350    );
351   
352    $errors = array();
353   
354    // check list of tables
355    $existing_tables = $DB->list_tables();
356
357    foreach ($db_schema as $table => $cols) {
358      $table = !empty($this->config['db_table_'.$table]) ? $this->config['db_table_'.$table] : $table;
359      if (!in_array($table, $existing_tables))
360        $errors[] = "Missing table ".$table;
361      // TODO: check cols and indices
362    }
363   
364    return !empty($errors) ? $errors : false;
365  }
366 
367  /**
368   * Compare the local database schema with the reference schema
369   * required for this version of Roundcube
370   *
371   * @param boolean True if the schema schould be updated
372   * @return boolean True if the schema is up-to-date, false if not or an error occured
373   */
374  function mdb2_schema_check($update = false)
375  {
376    if (!$this->configured)
377      return false;
378   
379    $options = array(
380      'use_transactions' => false,
381      'log_line_break' => "\n",
382      'idxname_format' => '%s',
383      'debug' => false,
384      'quote_identifier' => true,
385      'force_defaults' => false,
386      'portability' => true
387    );
388   
389    $dsnw = $this->config['db_dsnw'];
390    $schema = MDB2_Schema::factory($dsnw, $options);
391    $schema->db->supported['transactions'] = false;
392   
393    if (PEAR::isError($schema)) {
394      $this->raise_error(array('code' => $schema->getCode(), 'message' => $schema->getMessage() . ' ' . $schema->getUserInfo()));
395      return false;
396    }
397    else {
398      $definition = $schema->getDefinitionFromDatabase();
399      $definition['charset'] = 'utf8';
400     
401      if (PEAR::isError($definition)) {
402        $this->raise_error(array('code' => $definition->getCode(), 'message' => $definition->getMessage() . ' ' . $definition->getUserInfo()));
403        return false;
404      }
405     
406      // load reference schema
407      $dsn_arr = MDB2::parseDSN($this->config['db_dsnw']);
408
409      $ref_schema = INSTALL_PATH . 'SQL/' . $dsn_arr['phptype'] . '.schema.xml';
410     
411      if (is_readable($ref_schema)) {
412        $reference = $schema->parseDatabaseDefinition($ref_schema, false, array(), $schema->options['fail_on_invalid_names']);
413       
414        if (PEAR::isError($reference)) {
415          $this->raise_error(array('code' => $reference->getCode(), 'message' => $reference->getMessage() . ' ' . $reference->getUserInfo()));
416        }
417        else {
418          $diff = $schema->compareDefinitions($reference, $definition);
419         
420          if (empty($diff)) {
421            return true;
422          }
423          else if ($update) {
424            // update database schema with the diff from the above check
425            $success = $schema->alterDatabase($reference, $definition, $diff);
426           
427            if (PEAR::isError($success)) {
428              $this->raise_error(array('code' => $success->getCode(), 'message' => $success->getMessage() . ' ' . $success->getUserInfo()));
429            }
430            else
431              return true;
432          }
433          echo '<pre>'; var_dump($diff); echo '</pre>';
434          return false;
435        }
436      }
437      else
438        $this->raise_error(array('message' => "Could not find reference schema file ($ref_schema)"));
439        return false;
440    }
441   
442    return false;
443  }
444 
445 
446  /**
447   * Getter for the last error message
448   *
449   * @return string Error message or null if none exists
450   */
451  function get_error()
452  {
453      return $this->last_error['message'];
454  }
455 
456 
457  /**
458   * Return a list with all imap hosts configured
459   *
460   * @return array Clean list with imap hosts
461   */
462  function get_hostlist()
463  {
464    $default_hosts = (array)$this->getprop('default_host');
465    $out = array();
466   
467    foreach ($default_hosts as $key => $name) {
468      if (!empty($name))
469        $out[] = rcube_parse_host(is_numeric($key) ? $name : $key);
470    }
471   
472    return $out;
473  }
474 
475 
476  /**
477   * Display OK status
478   *
479   * @param string Test name
480   * @param string Confirm message
481   */
482  function pass($name, $message = '')
483  {
484    echo Q($name) . ':&nbsp; <span class="success">OK</span>';
485    $this->_showhint($message);
486  }
487 
488 
489  /**
490   * Display an error status and increase failure count
491   *
492   * @param string Test name
493   * @param string Error message
494   * @param string URL for details
495   */
496  function fail($name, $message = '', $url = '')
497  {
498    $this->failures++;
499   
500    echo Q($name) . ':&nbsp; <span class="fail">NOT OK</span>';
501    $this->_showhint($message, $url);
502  }
503
504
505  /**
506   * Display an error status for optional settings/features
507   *
508   * @param string Test name
509   * @param string Error message
510   * @param string URL for details
511   */
512  function optfail($name, $message = '', $url = '')
513  {
514    echo Q($name) . ':&nbsp; <span class="na">NOT OK</span>';
515    $this->_showhint($message, $url);
516  }
517 
518 
519  /**
520   * Display warning status
521   *
522   * @param string Test name
523   * @param string Warning message
524   * @param string URL for details
525   */
526  function na($name, $message = '', $url = '')
527  {
528    echo Q($name) . ':&nbsp; <span class="na">NOT AVAILABLE</span>';
529    $this->_showhint($message, $url);
530  }
531 
532 
533  function _showhint($message, $url = '')
534  {
535    $hint = Q($message);
536   
537    if ($url)
538      $hint .= ($hint ? '; ' : '') . 'See <a href="' . Q($url) . '" target="_blank">' . Q($url) . '</a>';
539     
540    if ($hint)
541      echo '<span class="indent">(' . $hint . ')</span>';
542  }
543 
544 
545  static function _clean_array($arr)
546  {
547    $out = array();
548   
549    foreach (array_unique($arr) as $k => $val) {
550      if (!empty($val)) {
551        if (is_numeric($k))
552          $out[] = $val;
553        else
554          $out[$k] = $val;
555      }
556    }
557   
558    return $out;
559  }
560 
561 
562  static function _dump_var($var) {
563    if (is_array($var)) {
564      if (empty($var)) {
565        return 'array()';
566      }
567      else {  // check if all keys are numeric
568        $isnum = true;
569        foreach ($var as $key => $value) {
570          if (!is_numeric($key)) {
571            $isnum = false;
572            break;
573          }
574        }
575       
576        if ($isnum)
577          return 'array(' . join(', ', array_map(array('rcube_install', '_dump_var'), $var)) . ')';
578      }
579    }
580   
581    return var_export($var, true);
582  }
583 
584 
585  /**
586   * Initialize the database with the according schema
587   *
588   * @param object rcube_db Database connection
589   * @return boolen True on success, False on error
590   */
591  function init_db($DB)
592  {
593    $db_map = array('pgsql' => 'postgres', 'mysqli' => 'mysql');
594    $engine = isset($db_map[$DB->db_provider]) ? $db_map[$DB->db_provider] : $DB->db_provider;
595   
596    // read schema file from /SQL/*
597    $fname = "../SQL/$engine.initial.sql";
598    if ($lines = @file($fname, FILE_SKIP_EMPTY_LINES)) {
599      $buff = '';
600      foreach ($lines as $i => $line) {
601        if (preg_match('/^--/', $line))
602          continue;
603         
604        $buff .= $line . "\n";
605        if (preg_match('/;$/', trim($line))) {
606          $DB->query($buff);
607          $buff = '';
608          if ($this->get_error())
609            break;
610        }
611      }
612    }
613    else {
614      $this->fail('DB Schema', "Cannot read the schema file: $fname");
615      return false;
616    }
617   
618    if ($err = $this->get_error()) {
619      $this->fail('DB Schema', "Error creating database schema: $err");
620      return false;
621    }
622
623    return true;
624  }
625 
626  /**
627   * Handler for Roundcube errors
628   */
629  function raise_error($p)
630  {
631      $this->last_error = $p;
632  }
633 
634 
635  /**
636   * Generarte a ramdom string to be used as encryption key
637   *
638   * @param int Key length
639   * @return string The generated random string
640   * @static
641   */
642  function random_key($length)
643  {
644    $alpha = 'ABCDEFGHIJKLMNOPQERSTUVXYZabcdefghijklmnopqrtsuvwxyz0123456789+*%&?!$-_=';
645    $out = '';
646   
647    for ($i=0; $i < $length; $i++)
648      $out .= $alpha{rand(0, strlen($alpha)-1)};
649   
650    return $out;
651  }
652 
653}
654
Note: See TracBrowser for help on using the repository browser.