thomascube
2008-10-14 871ca9adfedfa0aedf1994af579a5ea6715cff5f
commit | author | age
354978 1 <?php
T 2
3 /*
4  +-----------------------------------------------------------------------+
5  | rcube_install.php                                                     |
6  |                                                                       |
7  | This file is part of the RoundCube Webmail package                    |
8  | Copyright (C) 2008, 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  */
24 class rcube_install
25 {
26   var $step;
237119 27   var $is_post = false;
354978 28   var $failures = 0;
c5042d 29   var $config = array();
b3f9df 30   var $configured = false;
c5042d 31   var $last_error = null;
ad43e6 32   var $email_pattern = '([a-z0-9][a-z0-9\-\.\+\_]*@[a-z0-9]([a-z0-9\-][.]?)*[a-z0-9])';
871ca9 33   var $bool_config_props = array();
e10712 34
T 35   var $obsolete_config = array('db_backend');
36   var $replaced_config = array('skin_path' => 'skin', 'locale_string' => 'language');
37   
38   // these config options are optional or can be set to null
39   var $optional_config = array(
40     'log_driver', 'syslog_id', 'syslog_facility', 'imap_auth_type',
41     'smtp_helo_host', 'sendmail_delay', 'double_auth', 'language',
42     'mail_header_delimiter', 'create_default_folders',
43     'quota_zero_as_unlimited', 'spellcheck_uri', 'spellcheck_languages',
44     'http_received_header', 'session_domain', 'mime_magic', 'log_logins',
45     'enable_installer', 'skin_include_php');
354978 46   
T 47   /**
48    * Constructor
49    */
50   function rcube_install()
51   {
52     $this->step = intval($_REQUEST['_step']);
237119 53     $this->is_post = $_SERVER['REQUEST_METHOD'] == 'POST';
354978 54   }
T 55   
c5042d 56   /**
T 57    * Singleton getter
58    */
59   function get_instance()
60   {
61     static $inst;
62     
63     if (!$inst)
64       $inst = new rcube_install();
65     
66     return $inst;
67   }
354978 68   
T 69   /**
c5042d 70    * Read the default config files and store properties
354978 71    */
190e97 72   function load_defaults()
354978 73   {
c5042d 74     $this->_load_config('.php.dist');
T 75   }
76
77
78   /**
79    * Read the local config files and store properties
80    */
81   function load_config()
82   {
b3f9df 83     $this->config = array();
c5042d 84     $this->_load_config('.php');
b3f9df 85     $this->configured = !empty($this->config);
c5042d 86   }
T 87
88   /**
89    * Read the default config file and store properties
90    * @access private
91    */
92   function _load_config($suffix)
93   {
bba657 94     @include RCMAIL_CONFIG_DIR . '/main.inc' . $suffix;
354978 95     if (is_array($rcmail_config)) {
190e97 96       $this->config += $rcmail_config;
354978 97     }
T 98       
bba657 99     @include RCMAIL_CONFIG_DIR . '/db.inc'. $suffix;
354978 100     if (is_array($rcmail_config)) {
c5042d 101       $this->config += $rcmail_config;
354978 102     }
T 103   }
104   
105   
106   /**
107    * Getter for a certain config property
108    *
109    * @param string Property name
ad43e6 110    * @param string Default value
354978 111    * @return string The property value
T 112    */
fa7539 113   function getprop($name, $default = '')
354978 114   {
b77d0d 115     $value = $this->config[$name];
354978 116     
ccb412 117     if ($name == 'des_key' && !$this->configured && !isset($_REQUEST["_$name"]))
807d17 118       $value = rcube_install::random_key(24);
354978 119     
fa7539 120     return $value !== null && $value !== '' ? $value : $default;
354978 121   }
T 122   
123   
124   /**
125    * Take the default config file and replace the parameters
126    * with the submitted form data
127    *
128    * @param string Which config file (either 'main' or 'db')
129    * @return string The complete config file content
130    */
e10712 131   function create_config($which, $force = false)
354978 132   {
T 133     $out = file_get_contents("../config/{$which}.inc.php.dist");
134     
135     if (!$out)
136       return '[Warning: could not read the template file]';
0c3bde 137
c5042d 138     foreach ($this->config as $prop => $default) {
871ca9 139       $value = (isset($_POST["_$prop"]) || $this->bool_config_props[$prop]) ? $_POST["_$prop"] : $default;
354978 140       
T 141       // convert some form data
b77d0d 142       if ($prop == 'debug_level') {
354978 143         $val = 0;
7d7f67 144         if (is_array($value))
T 145           foreach ($value as $dbgval)
b77d0d 146             $val += intval($dbgval);
7d7f67 147         $value = $val;
354978 148       }
0c3bde 149       else if ($which == 'db' && $prop == 'db_dsnw' && !empty($_POST['_dbtype'])) {
237119 150         if ($_POST['_dbtype'] == 'sqlite')
T 151           $value = sprintf('%s://%s?mode=0646', $_POST['_dbtype'], $_POST['_dbname']{0} == '/' ? '/' . $_POST['_dbname'] : $_POST['_dbname']);
152         else
b61965 153           $value = sprintf('%s://%s:%s@%s/%s', $_POST['_dbtype'], 
7d7f67 154             rawurlencode($_POST['_dbuser']), rawurlencode($_POST['_dbpass']), $_POST['_dbhost'], $_POST['_dbname']);
c5042d 155       }
T 156       else if ($prop == 'smtp_auth_type' && $value == '0') {
157         $value = '';
158       }
159       else if ($prop == 'default_host' && is_array($value)) {
807d17 160         $value = rcube_install::_clean_array($value);
c5042d 161         if (count($value) <= 1)
T 162           $value = $value[0];
163       }
ccb412 164       else if ($prop == 'pagesize') {
T 165         $value = max(2, intval($value));
166       }
c5042d 167       else if ($prop == 'smtp_user' && !empty($_POST['_smtp_user_u'])) {
T 168         $value = '%u';
169       }
170       else if ($prop == 'smtp_pass' && !empty($_POST['_smtp_user_u'])) {
171         $value = '%p';
172       }
173       else if (is_bool($default)) {
27564f 174         $value = (bool)$value;
T 175       }
176       else if (is_numeric($value)) {
177         $value = intval($value);
c5042d 178       }
T 179       
180       // skip this property
e10712 181       if (!$force && ($value == $default))
c5042d 182         continue;
b77d0d 183
A 184       // save change
185       $this->config[$prop] = $value;
186
354978 187       // replace the matching line in config file
T 188       $out = preg_replace(
189         '/(\$rcmail_config\[\''.preg_quote($prop).'\'\])\s+=\s+(.+);/Uie',
190         "'\\1 = ' . var_export(\$value, true) . ';'",
191         $out);
192     }
0c3bde 193
967b34 194     return trim($out);
c5042d 195   }
e10712 196
T 197
198   /**
199    * Check the current configuration for missing properties
200    * and deprecated or obsolete settings
201    *
202    * @return array List with problems detected
203    */
204   function check_config()
205   {
206     $this->config = array();
207     $this->load_defaults();
208     $defaults = $this->config;
209     
210     $this->load_config();
211     if (!$this->configured)
212       return null;
213     
214     $out = $seen = array();
215     $optional = array_flip($this->optional_config);
216     
217     // ireate over the current configuration
218     foreach ($this->config as $prop => $value) {
219       if ($replacement = $this->replaced_config[$prop]) {
220         $out['replaced'][] = array('prop' => $prop, 'replacement' => $replacement);
221         $seen[$replacement] = true;
222       }
223       else if (!$seen[$prop] && in_array($prop, $this->obsolete_config)) {
224         $out['obsolete'][] = array('prop' => $prop);
225         $seen[$prop] = true;
226       }
227     }
228     
229     // iterate over default config
230     foreach ($defaults as $prop => $value) {
231       if (!$seen[$prop] && !isset($this->config[$prop]) && !isset($optional[$prop]))
232         $out['missing'][] = array('prop' => $prop);
233     }
234     
871ca9 235     // check config dependencies and contradictions
T 236     if ($this->config['enable_spellcheck'] && $this->config['spellcheck_engine'] == 'pspell') {
237       if (!extension_loaded('pspell')) {
238         $out['dependencies'][] = array('prop' => 'spellcheck_engine',
239           'explain' => 'This requires the <tt>pspell</tt> extension which could not be loaded.');
240       }
241       if (empty($this->config['spellcheck_languages'])) {
242         $out['dependencies'][] = array('prop' => 'spellcheck_languages',
243           'explain' => 'You should specify the list of languages supported by your local pspell installation.');
244       }
245     }
246     
247     if ($this->config['log_driver'] == 'syslog') {
248       if (!function_exists('openlog')) {
249         $out['dependencies'][] = array('prop' => 'log_driver',
250           'explain' => 'This requires the <tt>sylog</tt> extension which could not be loaded.');
251       }
252       if (empty($this->config['syslog_id'])) {
253         $out['dependencies'][] = array('prop' => 'syslog_id',
254           'explain' => 'Using <tt>syslog</tt> for logging requires a syslog ID to be configured');
255       }
256       
257     }
258     
e10712 259     return $out;
T 260   }
261   
262   
263   /**
264    * Merge the current configuration with the defaults
265    * and copy replaced values to the new options.
266    */
267   function merge_config()
268   {
269     $current = $this->config;
270     $this->config = array();
271     $this->load_defaults();
272     
273     foreach ($this->replaced_config as $prop => $replacement)
274       if (isset($current[$prop])) {
275         if ($prop == 'skin_path')
276           $this->config[$replacement] = preg_replace('#skins/(\w+)/?$#', '\\1', $current[$prop]);
277         else
278           $this->config[$replacement] = $current[$prop];
279         
280         unset($current[$prop]);
281     }
282     
283     foreach ($this->obsolete_config as $prop) {
284       unset($current[$prop]);
285     }
286     
287     $this->config  = array_merge($current, $this->config);
288   }
c5042d 289   
T 290   
291   /**
871ca9 292    * Compare the local database schema with the reference schema
T 293    * required for this version of RoundCube
294    *
295    * @param boolean True if the schema schould be updated
296    * @return boolean True if the schema is up-to-date, false if not or an error occured
297    */
298   function db_schema_check($update = false)
299   {
300     if (!$this->configured)
301       return false;
302     
303     $options = array(
304       'use_transactions' => false,
305       'log_line_break' => "\n",
306       'idxname_format' => '%s',
307       'debug' => false,
308       'quote_identifier' => true,
309       'force_defaults' => false,
310       'portability' => true
311     );
312     
313     $schema =& MDB2_Schema::factory($this->config['db_dsnw'], $options);
314     $schema->db->supported['transactions'] = false;
315     
316     if (PEAR::isError($schema)) {
317       $this->raise_error(array('code' => $schema->getCode(), 'message' => $schema->getMessage() . ' ' . $schema->getUserInfo()));
318       return false;
319     }
320     else {
321       $definition = $schema->getDefinitionFromDatabase();
322       $definition['charset'] = 'utf8';
323       
324       if (PEAR::isError($definition)) {
325         $this->raise_error(array('code' => $definition->getCode(), 'message' => $definition->getMessage() . ' ' . $definition->getUserInfo()));
326         return false;
327       }
328       
329       // load reference schema
330       $dsn = MDB2::parseDSN($this->config['db_dsnw']);
331       $ref_schema = INSTALL_PATH . 'SQL/' . $dsn['phptype'] . '.schema.xml';
332       
333       if (is_file($ref_schema)) {
334         $reference = $schema->parseDatabaseDefinition($ref_schema, false, array(), $schema->options['fail_on_invalid_names']);
335         
336         if (PEAR::isError($reference)) {
337           $this->raise_error(array('code' => $reference->getCode(), 'message' => $reference->getMessage() . ' ' . $reference->getUserInfo()));
338         }
339         else {
340           $diff = $schema->compareDefinitions($reference, $definition);
341           
342           if (empty($diff)) {
343             return true;
344           }
345           else if ($update) {
346             // update database schema with the diff from the above check
347             $success = $schema->alterDatabase($reference, $definition, $diff);
348             
349             if (PEAR::isError($success)) {
350               $this->raise_error(array('code' => $success->getCode(), 'message' => $success->getMessage() . ' ' . $success->getUserInfo()));
351             }
352             else
353               return true;
354           }
355           echo '<pre>'; var_dump($diff); echo '</pre>';
356           return false;
357         }
358       }
359       else
360         $this->raise_error(array('message' => "Could not find reference schema file ($ref_schema)"));
361         return false;
362     }
363     
364     return false;
365   }
366   
367   
368   /**
c5042d 369    * Getter for the last error message
T 370    *
371    * @return string Error message or null if none exists
372    */
373   function get_error()
374   {
375       return $this->last_error['message'];
354978 376   }
T 377   
378   
379   /**
112c54 380    * Return a list with all imap hosts configured
T 381    *
382    * @return array Clean list with imap hosts
383    */
384   function get_hostlist()
385   {
386     $default_hosts = (array)$this->getprop('default_host');
387     $out = array();
388     
389     foreach ($default_hosts as $key => $name) {
390       if (!empty($name))
391         $out[] = is_numeric($key) ? $name : $key;
392     }
393     
394     return $out;
395   }
396   
397   
398   /**
354978 399    * Display OK status
T 400    *
401    * @param string Test name
402    * @param string Confirm message
403    */
404   function pass($name, $message = '')
405   {
406     echo Q($name) . ':&nbsp; <span class="success">OK</span>';
6557d3 407     $this->_showhint($message);
354978 408   }
T 409   
410   
411   /**
412    * Display an error status and increase failure count
413    *
414    * @param string Test name
415    * @param string Error message
416    * @param string URL for details
417    */
418   function fail($name, $message = '', $url = '')
419   {
420     $this->failures++;
421     
422     echo Q($name) . ':&nbsp; <span class="fail">NOT OK</span>';
6557d3 423     $this->_showhint($message, $url);
354978 424   }
T 425   
426   
427   /**
428    * Display warning status
429    *
430    * @param string Test name
431    * @param string Warning message
432    * @param string URL for details
433    */
6557d3 434   function na($name, $message = '', $url = '')
354978 435   {
6557d3 436     echo Q($name) . ':&nbsp; <span class="na">NOT AVAILABLE</span>';
T 437     $this->_showhint($message, $url);
438   }
439   
440   
441   function _showhint($message, $url = '')
442   {
443     $hint = Q($message);
444     
354978 445     if ($url)
6557d3 446       $hint .= ($hint ? '; ' : '') . 'See <a href="' . Q($url) . '" target="_blank">' . Q($url) . '</a>';
T 447       
448     if ($hint)
449       echo '<span class="indent">(' . $hint . ')</span>';
354978 450   }
T 451   
452   
c5042d 453   function _clean_array($arr)
T 454   {
455     $out = array();
456     
457     foreach (array_unique($arr) as $i => $val)
458       if (!empty($val))
459         $out[] = $val;
460     
461     return $out;
462   }
463   
190e97 464   
T 465   /**
466    * Initialize the database with the according schema
467    *
468    * @param object rcube_db Database connection
469    * @return boolen True on success, False on error
470    */
471   function init_db($DB)
472   {
473     $db_map = array('pgsql' => 'postgres', 'mysqli' => 'mysql');
474     $engine = isset($db_map[$DB->db_provider]) ? $db_map[$DB->db_provider] : $DB->db_provider;
475     
476     // find out db version
477     if ($engine == 'mysql') {
478       $DB->query('SELECT VERSION() AS version');
479       $sql_arr = $DB->fetch_assoc();
480       $version = floatval($sql_arr['version']);
481       
482       if ($version >= 4.1)
483         $engine = 'mysql5';
484     }
485     
486     // read schema file from /SQL/*
487     $fname = "../SQL/$engine.initial.sql";
488     if ($lines = @file($fname, FILE_SKIP_EMPTY_LINES)) {
489       $buff = '';
490       foreach ($lines as $i => $line) {
491         if (eregi('^--', $line))
492           continue;
493           
494         $buff .= $line . "\n";
495         if (eregi(';$', trim($line))) {
496           $DB->query($buff);
497           $buff = '';
c0dc90 498           if ($this->get_error())
T 499             break;
190e97 500         }
T 501       }
502     }
503     else {
504       $this->fail('DB Schema', "Cannot read the schema file: $fname");
505       return false;
506     }
507     
508     if ($err = $this->get_error()) {
509       $this->fail('DB Schema', "Error creating database schema: $err");
510       return false;
511     }
512
513     return true;
514   }
515   
c5042d 516   /**
T 517    * Handler for RoundCube errors
518    */
519   function raise_error($p)
520   {
521       $this->last_error = $p;
522   }
523   
524   
354978 525   /**
T 526    * Generarte a ramdom string to be used as encryption key
527    *
528    * @param int Key length
529    * @return string The generated random string
530    * @static
531    */
532   function random_key($length)
533   {
534     $alpha = 'ABCDEFGHIJKLMNOPQERSTUVXYZabcdefghijklmnopqrtsuvwxyz0123456789+*%&?!$-_=';
535     $out = '';
536     
537     for ($i=0; $i < $length; $i++)
538       $out .= $alpha{rand(0, strlen($alpha)-1)};
539     
540     return $out;
541   }
542   
543 }
544
545
546 /**
547  * Shortcut function for htmlentities()
548  *
549  * @param string String to quote
550  * @return string The html-encoded string
551  */
552 function Q($string)
553 {
554   return htmlentities($string);
555 }
556
c5042d 557
T 558 /**
559  * Fake rinternal error handler to catch errors
560  */
561 function raise_error($p)
562 {
563   $rci = rcube_install::get_instance();
564   $rci->raise_error($p);
565 }
566