File caching backend is the default
[tine20] / tine20 / Setup / Controller.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Setup
6  * @subpackage  Controller
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Lars Kneschke <l.kneschke@metaways.de>
9  * @copyright   Copyright (c) 2008-2014 Metaways Infosystems GmbH (http://www.metaways.de)
10  *
11  * @todo        move $this->_db calls to backend class
12  */
13
14 /**
15  * php helpers
16  */
17 require_once dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'Tinebase' . DIRECTORY_SEPARATOR . 'Helper.php';
18
19 /**
20  * class to handle setup of Tine 2.0
21  *
22  * @package     Setup
23  * @subpackage  Controller
24  */
25 class Setup_Controller
26 {
27     /**
28      * holds the instance of the singleton
29      *
30      * @var Setup_Controller
31      */
32     private static $_instance = NULL;
33     
34     /**
35      * setup backend
36      *
37      * @var Setup_Backend_Interface
38      */
39     protected $_backend = NULL;
40     
41     /**
42      * the directory where applications are located
43      *
44      * @var string
45      */
46     protected $_baseDir;
47     
48     /**
49      * the email configs to get/set
50      *
51      * @var array
52      */
53     protected $_emailConfigKeys = array();
54     
55     /**
56      * number of updated apps
57      * 
58      * @var integer
59      */
60     protected $_updatedApplications = 0;
61     
62     /**
63      * is filesystem available
64      * 
65      * @var boolean
66      */
67     protected $_isFileSystemAvailable = null;
68     
69     /**
70      * don't clone. Use the singleton.
71      *
72      */
73     private function __clone() {}
74     
75     /**
76      * url to Tine 2.0 wiki
77      *
78      * @var string
79      */
80     protected $_helperLink = ' <a href="http://www.tine20.org/wiki/index.php/Admins/Install_Howto" target="_blank">Check the Tine 2.0 wiki for support.</a>';
81
82     /**
83      * the singleton pattern
84      *
85      * @return Setup_Controller
86      */
87     public static function getInstance()
88     {
89         if (self::$_instance === NULL) {
90             self::$_instance = new Setup_Controller;
91         }
92         
93         return self::$_instance;
94     }
95
96     /**
97      * the constructor
98      *
99      */
100     private function __construct()
101     {
102         // setup actions could take quite a while we try to set max execution time to unlimited
103         Setup_Core::setExecutionLifeTime(0);
104         
105         if (!defined('MAXLOOPCOUNT')) {
106             define('MAXLOOPCOUNT', 50);
107         }
108         
109         $this->_baseDir = dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR;
110         
111         if (Setup_Core::get(Setup_Core::CHECKDB)) {
112             $this->_db = Setup_Core::getDb();
113             $this->_backend = Setup_Backend_Factory::factory();
114         } else {
115             $this->_db = NULL;
116         }
117         
118         $this->_emailConfigKeys = array(
119             'imap'  => Tinebase_Config::IMAP,
120             'smtp'  => Tinebase_Config::SMTP,
121             'sieve' => Tinebase_Config::SIEVE,
122         );
123     }
124
125     /**
126      * check system/php requirements (env + ext check)
127      *
128      * @return array
129      *
130      * @todo add message to results array
131      */
132     public function checkRequirements()
133     {
134         $envCheck = $this->environmentCheck();
135         
136         $databaseCheck = $this->checkDatabase();
137         
138         $extCheck = new Setup_ExtCheck(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'essentials.xml');
139         $extResult = $extCheck->getData();
140
141         $result = array(
142             'success' => ($envCheck['success'] && $databaseCheck['success'] && $extResult['success']),
143             'results' => array_merge($envCheck['result'], $databaseCheck['result'], $extResult['result']),
144         );
145
146         $result['totalcount'] = count($result['results']);
147         
148         return $result;
149     }
150     
151     /**
152      * check which database extensions are available
153      *
154      * @return array
155      */
156     public function checkDatabase()
157     {
158         $result = array(
159             'result'  => array(),
160             'success' => false
161         );
162         
163         $loadedExtensions = get_loaded_extensions();
164         
165         if (! in_array('PDO', $loadedExtensions)) {
166             $result['result'][] = array(
167                 'key'       => 'Database',
168                 'value'     => FALSE,
169                 'message'   => "PDO extension not found."  . $this->_helperLink
170             );
171             
172             return $result;
173         }
174         
175         // check mysql requirements
176         $missingMysqlExtensions = array_diff(array('mysql', 'pdo_mysql'), $loadedExtensions);
177         
178         // check pgsql requirements
179         $missingPgsqlExtensions = array_diff(array('pgsql', 'pdo_pgsql'), $loadedExtensions);
180         
181         // check oracle requirements
182         $missingOracleExtensions = array_diff(array('oci8'), $loadedExtensions);
183
184         if (! empty($missingMysqlExtensions) && ! empty($missingPgsqlExtensions) && ! empty($missingOracleExtensions)) {
185             $result['result'][] = array(
186                 'key'       => 'Database',
187                 'value'     => FALSE,
188                 'message'   => 'Database extensions missing. For MySQL install: ' . implode(', ', $missingMysqlExtensions) . 
189                                ' For Oracle install: ' . implode(', ', $missingOracleExtensions) . 
190                                ' For PostgreSQL install: ' . implode(', ', $missingPgsqlExtensions) .
191                                $this->_helperLink
192             );
193             
194             return $result;
195         }
196         
197         $result['result'][] = array(
198             'key'       => 'Database',
199             'value'     => TRUE,
200             'message'   => 'Support for following databases enabled: ' . 
201                            (empty($missingMysqlExtensions) ? 'MySQL' : '') . ' ' .
202                            (empty($missingOracleExtensions) ? 'Oracle' : '') . ' ' .
203                            (empty($missingPgsqlExtensions) ? 'PostgreSQL' : '') . ' '
204         );
205         $result['success'] = TRUE;
206         
207         return $result;
208     }
209     
210     /**
211      * Check if logger is properly configured (or not configured at all)
212      *
213      * @return boolean
214      */
215     public function checkConfigLogger()
216     {
217         $config = Setup_Core::get(Setup_Core::CONFIG);
218         if (!isset($config->logger) || !$config->logger->active) {
219             return true;
220         } else {
221             return (
222                 isset($config->logger->filename)
223                 && (
224                     file_exists($config->logger->filename) && is_writable($config->logger->filename)
225                     || is_writable(dirname($config->logger->filename))
226                 )
227             );
228         }
229     }
230     
231     /**
232      * Check if caching is properly configured (or not configured at all)
233      *
234      * @return boolean
235      */
236     public function checkConfigCaching()
237     {
238         $result = FALSE;
239         
240         $config = Setup_Core::get(Setup_Core::CONFIG);
241         
242         if (! isset($config->caching) || !$config->caching->active) {
243             $result = TRUE;
244             
245         } else if (! isset($config->caching->backend) || ucfirst($config->caching->backend) === 'File') {
246             $result = $this->checkDir('path', 'caching', FALSE);
247             
248         } else if (ucfirst($config->caching->backend) === 'Redis') {
249             $result = $this->_checkRedisConnect(isset($config->caching->redis) ? $config->caching->redis->toArray() : array());
250             
251         } else if (ucfirst($config->caching->backend) === 'Memcached') {
252             $result = $this->_checkMemcacheConnect(isset($config->caching->memcached) ? $config->caching->memcached->toArray() : array());
253             
254         }
255         
256         return $result;
257     }
258     
259     /**
260      * checks redis extension and connection
261      * 
262      * @param array $config
263      * @return boolean
264      */
265     protected function _checkRedisConnect($config)
266     {
267         if (! extension_loaded('redis')) {
268             Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' redis extension not loaded');
269             return FALSE;
270         }
271         $redis = new Redis;
272         $host = isset($config['host']) ? $config['host'] : 'localhost';
273         $port = isset($config['port']) ? $config['port'] : 6379;
274         
275         $result = $redis->connect($host, $port);
276         if ($result) {
277             $redis->close();
278         } else {
279             Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Could not connect to redis server at ' . $host . ':' . $port);
280         }
281         
282         return $result;
283     }
284     
285     /**
286      * checks memcached extension and connection
287      * 
288      * @param array $config
289      * @return boolean
290      */
291     protected function _checkMemcacheConnect($config)
292     {
293         if (! extension_loaded('memcache')) {
294             Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' memcache extension not loaded');
295             return FALSE;
296         }
297         $memcache = new Memcache;
298         $host = isset($config['host']) ? $config['host'] : 'localhost';
299         $port = isset($config['port']) ? $config['port'] : 11211;
300         $result = $memcache->connect($host, $port);
301         
302         return $result;
303     }
304     
305     /**
306      * Check if queue is properly configured (or not configured at all)
307      *
308      * @return boolean
309      */
310     public function checkConfigQueue()
311     {
312         $config = Setup_Core::get(Setup_Core::CONFIG);
313         if (! isset($config->actionqueue) || ! $config->actionqueue->active) {
314             $result = TRUE;
315         } else {
316             $result = $this->_checkRedisConnect($config->actionqueue->toArray());
317         }
318         
319         return $result;
320     }
321     
322     /**
323      * check config session
324      * 
325      * @return boolean
326      */
327     public function checkConfigSession()
328     {
329         $result = FALSE;
330         $config = Setup_Core::get(Setup_Core::CONFIG);
331         if (! isset($config->session) || !$config->session->active) {
332             return TRUE;
333         } else if (ucfirst($config->session->backend) === 'File') {
334             return $this->checkDir('path', 'session', FALSE);
335         } else if (ucfirst($config->session->backend) === 'Redis') {
336             $result = $this->_checkRedisConnect($config->session->toArray());
337         }
338         
339         return $result;
340     }
341     
342     /**
343      * checks if path in config is writable
344      *
345      * @param string $_name
346      * @param string $_group
347      * @return boolean
348      */
349     public function checkDir($_name, $_group = NULL, $allowEmptyPath = TRUE)
350     {
351         $config = $this->getConfigData();
352         if ($_group !== NULL && (isset($config[$_group]) || array_key_exists($_group, $config))) {
353             $config = $config[$_group];
354         }
355         
356         $path = (isset($config[$_name]) || array_key_exists($_name, $config)) ? $config[$_name] : false;
357         if (empty($path)) {
358             return $allowEmptyPath;
359         } else {
360             return @is_writable($path);
361         }
362     }
363     
364     /**
365      * get list of applications as found in the filesystem
366      *
367      * @return array appName => setupXML
368      */
369     public function getInstallableApplications()
370     {
371         // create Tinebase tables first
372         $applications = array('Tinebase' => $this->getSetupXml('Tinebase'));
373         
374         try {
375             $dirIterator = new DirectoryIterator($this->_baseDir);
376         } catch (Exception $e) {
377             Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Could not open base dir: ' . $this->_baseDir);
378             throw new Tinebase_Exception_AccessDenied('Could not open Tine 2.0 root directory.');
379         }
380         
381         foreach ($dirIterator as $item) {
382             $appName = $item->getFileName();
383             if($appName{0} != '.' && $appName != 'Tinebase' && $item->isDir()) {
384                 $fileName = $this->_baseDir . $item->getFileName() . '/Setup/setup.xml' ;
385                 if(file_exists($fileName)) {
386                     $applications[$item->getFileName()] = $this->getSetupXml($item->getFileName());
387                 }
388             }
389         }
390         
391         return $applications;
392     }
393     
394     /**
395      * updates installed applications. does nothing if no applications are installed
396      *
397      * @param Tinebase_Record_RecordSet $_applications
398      * @return  array   messages
399      */
400     public function updateApplications(Tinebase_Record_RecordSet $_applications)
401     {
402         $this->_updatedApplications = 0;
403         $smallestMajorVersion = NULL;
404         $biggestMajorVersion = NULL;
405         
406         //find smallest major version
407         foreach ($_applications as $application) {
408             if ($smallestMajorVersion === NULL || $application->getMajorVersion() < $smallestMajorVersion) {
409                 $smallestMajorVersion = $application->getMajorVersion();
410             }
411             if ($biggestMajorVersion === NULL || $application->getMajorVersion() > $biggestMajorVersion) {
412                 $biggestMajorVersion = $application->getMajorVersion();
413             }
414         }
415         
416         $messages = array();
417         
418         // update tinebase first (to biggest major version)
419         $tinebase = $_applications->filter('name', 'Tinebase')->getFirstRecord();
420         if (! empty($tinebase)) {
421             unset($_applications[$_applications->getIndexById($tinebase->getId())]);
422         
423             list($major, $minor) = explode('.', $this->getSetupXml('Tinebase')->version[0]);
424             Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Updating Tinebase to version ' . $major . '.' . $minor);
425             
426             for ($majorVersion = $tinebase->getMajorVersion(); $majorVersion <= $major; $majorVersion++) {
427                 $messages += $this->updateApplication($tinebase, $majorVersion);
428             }
429         }
430             
431         // update the rest
432         for ($majorVersion = $smallestMajorVersion; $majorVersion <= $biggestMajorVersion; $majorVersion++) {
433             foreach ($_applications as $application) {
434                 if ($application->getMajorVersion() <= $majorVersion) {
435                     $messages += $this->updateApplication($application, $majorVersion);
436                 }
437             }
438         }
439         
440         return array(
441             'messages' => $messages,
442             'updated'  => $this->_updatedApplications,
443         );
444     }    
445     
446     /**
447      * load the setup.xml file and returns a simplexml object
448      *
449      * @param string $_applicationName name of the application
450      * @return SimpleXMLElement
451      */
452     public function getSetupXml($_applicationName)
453     {
454         $setupXML = $this->_baseDir . ucfirst($_applicationName) . '/Setup/setup.xml';
455
456         if (!file_exists($setupXML)) {
457             throw new Setup_Exception_NotFound(ucfirst($_applicationName) . '/Setup/setup.xml not found. If application got renamed or deleted, re-run setup.php.');
458         }
459         
460         $xml = simplexml_load_file($setupXML);
461
462         return $xml;
463     }
464     
465     /**
466      * check update
467      *
468      * @param   Tinebase_Model_Application $_application
469      * @throws  Setup_Exception
470      */
471     public function checkUpdate(Tinebase_Model_Application $_application)
472     {
473         $xmlTables = $this->getSetupXml($_application->name);
474         if(isset($xmlTables->tables)) {
475             foreach ($xmlTables->tables[0] as $tableXML) {
476                 $table = Setup_Backend_Schema_Table_Factory::factory('Xml', $tableXML);
477                 if (true == $this->_backend->tableExists($table->name)) {
478                     try {
479                         $this->_backend->checkTable($table);
480                     } catch (Setup_Exception $e) {
481                         Setup_Core::getLogger()->error(__METHOD__ . '::' . __LINE__ . " Checking table failed with message '{$e->getMessage()}'");
482                     }
483                 } else {
484                     throw new Setup_Exception('Table ' . $table->name . ' for application' . $_application->name . " does not exist. \n<strong>Update broken</strong>");
485                 }
486             }
487         }
488     }
489     
490     /**
491      * update installed application
492      *
493      * @param   Tinebase_Model_Application    $_application
494      * @param   string    $_majorVersion
495      * @return  array   messages
496      * @throws  Setup_Exception if current app version is too high
497      */
498     public function updateApplication(Tinebase_Model_Application $_application, $_majorVersion)
499     {
500         $setupXml = $this->getSetupXml($_application->name);
501         $messages = array();
502         
503         switch(version_compare($_application->version, $setupXml->version)) {
504             case -1:
505                 $message = "Executing updates for " . $_application->name . " (starting at " . $_application->version . ")";
506                 
507                 $messages[] = $message;
508                 Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' ' . $message);
509
510                 list($fromMajorVersion, $fromMinorVersion) = explode('.', $_application->version);
511         
512                 $minor = $fromMinorVersion;
513                 
514                 $className = ucfirst($_application->name) . '_Setup_Update_Release' . $_majorVersion;
515                 if(! class_exists($className)) {
516                     Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
517                         . " update class {$className} does not exists, skipping release {$_majorVersion} for app {$_application->name}"
518                     );
519                 } else {
520                     $update = new $className($this->_backend);
521                 
522                     $classMethods = get_class_methods($update);
523               
524                     // we must do at least one update
525                     do {
526                         $functionName = 'update_' . $minor;
527                         
528                         try {
529                             $db = Setup_Core::getDb();
530                             $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
531                         
532                             Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
533                                 . ' Updating ' . $_application->name . ' - ' . $functionName
534                             );
535                             
536                             $update->$functionName();
537                         
538                             Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
539                 
540                         } catch (Exception $e) {
541                             Tinebase_TransactionManager::getInstance()->rollBack();
542                             Setup_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' ' . $e->getMessage());
543                             Setup_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' ' . $e->getTraceAsString());
544                             throw $e;
545                         }
546                             
547                         $minor++;
548                     } while(array_search('update_' . $minor, $classMethods) !== false);
549                 }
550                 
551                 $messages[] = "<strong> Updated " . $_application->name . " successfully to " .  $_majorVersion . '.' . $minor . "</strong>";
552                 
553                 // update app version
554                 $updatedApp = Tinebase_Application::getInstance()->getApplicationById($_application->getId());
555                 $_application->version = $updatedApp->version;
556                 Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Updated ' . $_application->name . " successfully to " .  $_application->version);
557                 $this->_updatedApplications++;
558                 
559                 break;
560                 
561             case 0:
562                 Setup_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' No update needed for ' . $_application->name);
563                 break;
564                 
565             case 1:
566                 throw new Setup_Exception('Current application version is higher than version from setup.xml: '
567                     . $_application->version . ' > ' . $setupXml->version
568                 );
569                 break;
570         }
571         
572         Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Clearing cache after update ...');
573         $this->_enableCaching();
574         Tinebase_Core::getCache()->clean(Zend_Cache::CLEANING_MODE_ALL);
575         
576         return $messages;
577     }
578
579     /**
580      * checks if update is required
581      *
582      * @return boolean
583      */
584     public function updateNeeded($_application)
585     {
586         try {
587             $setupXml = $this->getSetupXml($_application->name);
588         } catch (Setup_Exception_NotFound $senf) {
589             Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' ' . $senf->getMessage() . ' Disabling application "' . $_application->name . '".');
590             Tinebase_Application::getInstance()->setApplicationState(array($_application->getId()), Tinebase_Application::DISABLED);
591             return false;
592         }
593         
594         $updateNeeded = version_compare($_application->version, $setupXml->version);
595         
596         if($updateNeeded === -1) {
597             return true;
598         }
599         
600         return false;
601     }
602     
603     /**
604      * search for installed and installable applications
605      *
606      * @return array
607      */
608     public function searchApplications()
609     {
610         // get installable apps
611         $installable = $this->getInstallableApplications();
612         
613         // get installed apps
614         if (Setup_Core::get(Setup_Core::CHECKDB)) {
615             try {
616                 $installed = Tinebase_Application::getInstance()->getApplications(NULL, 'id')->toArray();
617                 
618                 // merge to create result array
619                 $applications = array();
620                 foreach ($installed as $application) {
621                     
622                     if (! (isset($installable[$application['name']]) || array_key_exists($application['name'], $installable))) {
623                         Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' App ' . $application['name'] . ' does not exist any more.');
624                         continue;
625                     }
626                     
627                     $depends = (array) $installable[$application['name']]->depends;
628                     if (isset($depends['application'])) {
629                         $depends = implode(', ', (array) $depends['application']);
630                     }
631                     
632                     $application['current_version'] = (string) $installable[$application['name']]->version;
633                     $application['install_status'] = (version_compare($application['version'], $application['current_version']) === -1) ? 'updateable' : 'uptodate';
634                     $application['depends'] = $depends;
635                     $applications[] = $application;
636                     unset($installable[$application['name']]);
637                 }
638             } catch (Zend_Db_Statement_Exception $zse) {
639                 // no tables exist
640             }
641         }
642         
643         foreach ($installable as $name => $setupXML) {
644             $depends = (array) $setupXML->depends;
645             if (isset($depends['application'])) {
646                 $depends = implode(', ', (array) $depends['application']);
647             }
648             
649             $applications[] = array(
650                 'name'              => $name,
651                 'current_version'   => (string) $setupXML->version,
652                 'install_status'    => 'uninstalled',
653                 'depends'           => $depends,
654             );
655         }
656         
657         return array(
658             'results'       => $applications,
659             'totalcount'    => count($applications)
660         );
661     }
662
663     /**
664      * checks if setup is required
665      *
666      * @return boolean
667      */
668     public function setupRequired()
669     {
670         $result = FALSE;
671         
672         // check if applications table exists / only if db available
673         if (Setup_Core::isRegistered(Setup_Core::DB)) {
674             try {
675                 $applicationTable = Setup_Core::getDb()->describeTable(SQL_TABLE_PREFIX . 'applications');
676                 if (empty($applicationTable)) {
677                     Setup_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Applications table empty');
678                     $result = TRUE;
679                 }
680             } catch (Zend_Db_Statement_Exception $zdse) {
681                 Setup_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' ' . $zdse->getMessage());
682                 $result = TRUE;
683             } catch (Zend_Db_Adapter_Exception $zdae) {
684                 Setup_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' ' . $zdae->getMessage());
685                 $result = TRUE;
686             }
687         }
688         
689         return $result;
690     }
691     
692     /**
693      * do php.ini environment check
694      *
695      * @return array
696      */
697     public function environmentCheck()
698     {
699         $result = array();
700         $message = array();
701         $success = TRUE;
702         
703         
704         
705         // check php environment
706         $requiredIniSettings = array(
707             'magic_quotes_sybase'  => 0,
708             'magic_quotes_gpc'     => 0,
709             'magic_quotes_runtime' => 0,
710             'mbstring.func_overload' => 0,
711             'eaccelerator.enable' => 0,
712             'memory_limit' => '48M'
713         );
714         
715         foreach ($requiredIniSettings as $variable => $newValue) {
716             $oldValue = ini_get($variable);
717             
718             if ($variable == 'memory_limit') {
719                 $required = Tinebase_Helper::convertToBytes($newValue);
720                 $set = Tinebase_Helper::convertToBytes($oldValue);
721                 
722                 if ( $set < $required) {
723                     $result[] = array(
724                         'key'       => $variable,
725                         'value'     => FALSE,
726                         'message'   => "You need to set $variable equal or greater than $required (now: $set)." . $this->_helperLink
727                     );
728                     $success = FALSE;
729                 }
730
731             } elseif ($oldValue != $newValue) {
732                 if (ini_set($variable, $newValue) === false) {
733                     $result[] = array(
734                         'key'       => $variable,
735                         'value'     => FALSE,
736                         'message'   => "You need to set $variable from $oldValue to $newValue."  . $this->_helperLink
737                     );
738                     $success = FALSE;
739                 }
740             } else {
741                 $result[] = array(
742                     'key'       => $variable,
743                     'value'     => TRUE,
744                     'message'   => ''
745                 );
746             }
747         }
748         
749         return array(
750             'result'        => $result,
751             'success'       => $success,
752         );
753     }
754     
755     /**
756      * get config file default values
757      *
758      * @return array
759      */
760     public function getConfigDefaults()
761     {
762         $defaultPath = Setup_Core::guessTempDir();
763         
764         $result = array(
765             'database' => array(
766                 'host'  => 'localhost',
767                 'dbname' => 'tine20',
768                 'username' => 'tine20',
769                 'password' => '',
770                 'adapter' => 'pdo_mysql',
771                 'tableprefix' => 'tine20_',
772                 'port'          => 3306
773             ),
774             'logger' => array(
775                 'filename' => $defaultPath . DIRECTORY_SEPARATOR . 'tine20.log',
776                 'priority' => '5'
777             ),
778             'caching' => array(
779                'active' => 1,
780                'lifetime' => 3600,
781                'backend' => 'File',
782                'path' => $defaultPath,
783             ),
784             'tmpdir' => $defaultPath,
785             'session' => array(
786                 'path'      => Tinebase_Session::getSessionDir(),
787                 'liftime'   => 86400,
788             ),
789         );
790         
791         return $result;
792     }
793
794     /**
795      * get config file values
796      *
797      * @return array
798      */
799     public function getConfigData()
800     {
801         $configArray = Setup_Core::get(Setup_Core::CONFIG)->toArray();
802         
803         #####################################
804         # LEGACY/COMPATIBILITY:
805         # (1) had to rename session.save_path key to sessiondir because otherwise the
806         # generic save config method would interpret the "_" as array key/value seperator
807         # (2) moved session config to subgroup 'session'
808         if (empty($configArray['session']) || empty($configArray['session']['path'])) {
809             foreach (array('session.save_path', 'sessiondir') as $deprecatedSessionDir) {
810                 $sessionDir = (isset($configArray[$deprecatedSessionDir]) || array_key_exists($deprecatedSessionDir, $configArray)) ? $configArray[$deprecatedSessionDir] : '';
811                 if (! empty($sessionDir)) {
812                     if (empty($configArray['session'])) {
813                         $configArray['session'] = array();
814                     }
815                     $configArray['session']['path'] = $sessionDir;
816                     Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . " config.inc.php key '{$deprecatedSessionDir}' should be renamed to 'path' and moved to 'session' group.");
817                 }
818             }
819         }
820         #####################################
821         
822         return $configArray;
823     }
824     
825     /**
826      * save data to config file
827      *
828      * @param array   $_data
829      * @param boolean $_merge
830      */
831     public function saveConfigData($_data, $_merge = TRUE)
832     {
833         if (!empty($_data['setupuser']['password']) && !Setup_Auth::isMd5($_data['setupuser']['password'])) {
834             $password = $_data['setupuser']['password'];
835             $_data['setupuser']['password'] = md5($_data['setupuser']['password']);
836         }
837         if (Setup_Core::configFileExists() && !Setup_Core::configFileWritable()) {
838             throw new Setup_Exception('Config File is not writeable.');
839         }
840         
841         if (Setup_Core::configFileExists()) {
842             $doLogin = FALSE;
843             $filename = Setup_Core::getConfigFilePath();
844         } else {
845             $doLogin = TRUE;
846             $filename = dirname(__FILE__) . '/../config.inc.php';
847         }
848         
849         $config = $this->writeConfigToFile($_data, $_merge, $filename);
850         
851         Setup_Core::set(Setup_Core::CONFIG, $config);
852         
853         Setup_Core::setupLogger();
854         
855         if ($doLogin && isset($password)) {
856             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Create session for setup user ' . $_data['setupuser']['username']);
857             $this->login($_data['setupuser']['username'], $password);
858         }
859     }
860     
861     /**
862      * write config to a file
863      *
864      * @param array $_data
865      * @param boolean $_merge
866      * @param string $_filename
867      * @return Zend_Config
868      */
869     public function writeConfigToFile($_data, $_merge, $_filename)
870     {
871         // merge config data and active config
872         if ($_merge) {
873             $activeConfig = Setup_Core::get(Setup_Core::CONFIG);
874             $config = new Zend_Config($activeConfig->toArray(), true);
875             $config->merge(new Zend_Config($_data));
876         } else {
877             $config = new Zend_Config($_data);
878         }
879         
880         // write to file
881         Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Updating config.inc.php');
882         $writer = new Zend_Config_Writer_Array(array(
883             'config'   => $config,
884             'filename' => $_filename,
885         ));
886         $writer->write();
887         
888         return $config;
889     }
890     
891     /**
892      * load authentication data
893      *
894      * @return array
895      */
896     public function loadAuthenticationData()
897     {
898         return array(
899             'authentication'    => $this->_getAuthProviderData(),
900             'accounts'          => $this->_getAccountsStorageData(),
901             'redirectSettings'  => $this->_getRedirectSettings(),
902             'password'          => $this->_getPasswordSettings(),
903             'saveusername'      => $this->_getReuseUsernameSettings()
904         );
905     }
906     
907     /**
908      * Update authentication data
909      *
910      * Needs Tinebase tables to store the data, therefore
911      * installs Tinebase if it is not already installed
912      *
913      * @param array $_authenticationData
914      *
915      * @return bool
916      */
917     public function saveAuthentication($_authenticationData)
918     {
919         if ($this->isInstalled('Tinebase')) {
920             // NOTE: Tinebase_Setup_Initialiser calls this function again so
921             //       we come to this point on initial installation _and_ update
922             $this->_updateAuthentication($_authenticationData);
923         } else {
924             $installationOptions = array('authenticationData' => $_authenticationData);
925             $this->installApplications(array('Tinebase'), $installationOptions);
926         }
927     }
928
929     /**
930      * Save {@param $_authenticationData} to config file
931      *
932      * @param array $_authenticationData [hash containing settings for authentication and accountsStorage]
933      * @return void
934      */
935     protected function _updateAuthentication($_authenticationData)
936     {
937         // this is a dangerous TRACE as there might be passwords in here!
938         //if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_authenticationData, TRUE));
939
940         $this->_enableCaching();
941         
942         if (isset($_authenticationData['authentication'])) {
943             $this->_updateAuthenticationProvider($_authenticationData['authentication']);
944         }
945         
946         if (isset($_authenticationData['accounts'])) {
947             $this->_updateAccountsStorage($_authenticationData['accounts']);
948         }
949         
950         if (isset($_authenticationData['redirectSettings'])) {
951             $this->_updateRedirectSettings($_authenticationData['redirectSettings']);
952         }
953         
954         if (isset($_authenticationData['password'])) {
955             $this->_updatePasswordSettings($_authenticationData['password']);
956         }
957         
958         if (isset($_authenticationData['saveusername'])) {
959             $this->_updateReuseUsername($_authenticationData['saveusername']);
960         }
961         
962         if (isset($_authenticationData['acceptedTermsVersion'])) {
963             $this->saveAcceptedTerms($_authenticationData['acceptedTermsVersion']);
964         }
965     }
966     
967     /**
968      * enable caching to make sure cache gets cleaned if config options change
969      */
970     protected function _enableCaching()
971     {
972         if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Activate caching backend if available ...');
973         
974         Tinebase_Core::setupCache();
975     }
976     
977     /**
978      * Update authentication provider
979      *
980      * @param array $_data
981      * @return void
982      */
983     protected function _updateAuthenticationProvider($_data)
984     {
985         Tinebase_Auth::setBackendType($_data['backend']);
986         $config = (isset($_data[$_data['backend']])) ? $_data[$_data['backend']] : $_data;
987         
988         $excludeKeys = array('adminLoginName', 'adminPassword', 'adminPasswordConfirmation');
989         foreach ($excludeKeys as $key) {
990             if ((isset($config[$key]) || array_key_exists($key, $config))) {
991                 unset($config[$key]);
992             }
993         }
994         
995         Tinebase_Auth::setBackendConfiguration($config, null, true);
996         Tinebase_Auth::saveBackendConfiguration();
997     }
998     
999     /**
1000      * Update accountsStorage
1001      *
1002      * @param array $_data
1003      * @return void
1004      */
1005     protected function _updateAccountsStorage($_data)
1006     {
1007         $originalBackend = Tinebase_User::getConfiguredBackend();
1008         $newBackend = $_data['backend'];
1009         
1010         Tinebase_User::setBackendType($_data['backend']);
1011         $config = (isset($_data[$_data['backend']])) ? $_data[$_data['backend']] : $_data;
1012         Tinebase_User::setBackendConfiguration($config, null, true);
1013         Tinebase_User::saveBackendConfiguration();
1014         
1015         if ($originalBackend != $newBackend && $this->isInstalled('Addressbook') && $originalBackend == Tinebase_User::SQL) {
1016             Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " Switching from $originalBackend to $newBackend account storage");
1017             try {
1018                 $db = Setup_Core::getDb();
1019                 $transactionId = Tinebase_TransactionManager::getInstance()->startTransaction($db);
1020                 $this->_migrateFromSqlAccountsStorage();
1021                 Tinebase_TransactionManager::getInstance()->commitTransaction($transactionId);
1022         
1023             } catch (Exception $e) {
1024                 Tinebase_TransactionManager::getInstance()->rollBack();
1025                 Setup_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' ' . $e->getMessage());
1026                 Setup_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' ' . $e->getTraceAsString());
1027                 
1028                 Tinebase_User::setBackendType($originalBackend);
1029                 Tinebase_User::saveBackendConfiguration();
1030                 
1031                 throw $e;
1032             }
1033         }
1034     }
1035     
1036     /**
1037      * migrate from SQL account storage to another one (for example LDAP)
1038      * - deletes all users, groups and roles because they will be
1039      *   imported from new accounts storage backend
1040      */
1041     protected function _migrateFromSqlAccountsStorage()
1042     {
1043         Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Deleting all user accounts, groups, roles and rights');
1044         Tinebase_User::factory(Tinebase_User::SQL)->deleteAllUsers();
1045         
1046         $contactSQLBackend = new Addressbook_Backend_Sql();
1047         $allUserContactIds = $contactSQLBackend->search(new Addressbook_Model_ContactFilter(array('type' => 'user')), null, true);
1048         if (count($allUserContactIds) > 0) {
1049             $contactSQLBackend->delete($allUserContactIds);
1050         }
1051         
1052         
1053         Tinebase_Group::factory(Tinebase_Group::SQL)->deleteAllGroups();
1054         $listsSQLBackend = new Addressbook_Backend_List();
1055         $allGroupListIds = $listsSQLBackend->search(new Addressbook_Model_ListFilter(array('type' => 'group')), null, true);
1056         if (count($allGroupListIds) > 0) {
1057             $listsSQLBackend->delete($allGroupListIds);
1058         }
1059         
1060         
1061         $roles = Tinebase_Acl_Roles::getInstance();
1062         $roles->deleteAllRoles();
1063         
1064         // import users (from new backend) / create initial users (SQL)
1065         Tinebase_User::syncUsers(array('syncContactData' => TRUE));
1066         
1067         $roles->createInitialRoles();
1068         $applications = Tinebase_Application::getInstance()->getApplications(NULL, 'id');
1069         foreach ($applications as $application) {
1070              Setup_Initialize::initializeApplicationRights($application);
1071         }
1072     }
1073     
1074     /**
1075      * Update redirect settings
1076      *
1077      * @param array $_data
1078      * @return void
1079      */
1080     protected function _updateRedirectSettings($_data)
1081     {
1082         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_data, 1));
1083         $keys = array(Tinebase_Config::REDIRECTURL, Tinebase_Config::REDIRECTALWAYS, Tinebase_Config::REDIRECTTOREFERRER);
1084         foreach ($keys as $key) {
1085             if ((isset($_data[$key]) || array_key_exists($key, $_data))) {
1086                 if (strlen($_data[$key]) === 0) {
1087                     Tinebase_Config::getInstance()->delete($key);
1088                 } else {
1089                     Tinebase_Config::getInstance()->set($key, $_data[$key]);
1090                 }
1091             }
1092         }
1093     }
1094
1095         /**
1096      * update pw settings
1097      * 
1098      * @param array $data
1099      */
1100     protected function _updatePasswordSettings($data)
1101     {
1102         foreach ($data as $config => $value) {
1103             Tinebase_Config::getInstance()->set($config, $value);
1104         }
1105     }
1106     
1107     /**
1108      * update pw settings
1109      * 
1110      * @param array $data
1111      */
1112     protected function _updateReuseUsername($data)
1113     {
1114         foreach ($data as $config => $value) {
1115             Tinebase_Config::getInstance()->set($config, $value);
1116         }
1117     }
1118     
1119     /**
1120      *
1121      * get auth provider data
1122      *
1123      * @return array
1124      *
1125      * @todo get this from config table instead of file!
1126      */
1127     protected function _getAuthProviderData()
1128     {
1129         $result = Tinebase_Auth::getBackendConfigurationWithDefaults(Setup_Core::get(Setup_Core::CHECKDB));
1130         $result['backend'] = (Setup_Core::get(Setup_Core::CHECKDB)) ? Tinebase_Auth::getConfiguredBackend() : Tinebase_Auth::SQL;
1131
1132         return $result;
1133     }
1134     
1135     /**
1136      * get Accounts storage data
1137      *
1138      * @return array
1139      */
1140     protected function _getAccountsStorageData()
1141     {
1142         $result = Tinebase_User::getBackendConfigurationWithDefaults(Setup_Core::get(Setup_Core::CHECKDB));
1143         $result['backend'] = (Setup_Core::get(Setup_Core::CHECKDB)) ? Tinebase_User::getConfiguredBackend() : Tinebase_User::SQL;
1144
1145         return $result;
1146     }
1147     
1148     /**
1149      * Get redirect Settings from config table.
1150      * If Tinebase is not installed, default values will be returned.
1151      *
1152      * @return array
1153      */
1154     protected function _getRedirectSettings()
1155     {
1156         $return = array(
1157               Tinebase_Config::REDIRECTURL => '',
1158               Tinebase_Config::REDIRECTTOREFERRER => '0'
1159         );
1160         if (Setup_Core::get(Setup_Core::CHECKDB) && $this->isInstalled('Tinebase')) {
1161             $return[Tinebase_Config::REDIRECTURL] = Tinebase_Config::getInstance()->get(Tinebase_Config::REDIRECTURL, '');
1162             $return[Tinebase_Config::REDIRECTTOREFERRER] = Tinebase_Config::getInstance()->get(Tinebase_Config::REDIRECTTOREFERRER, '');
1163         }
1164         return $return;
1165     }
1166
1167     /**
1168      * get password settings
1169      * 
1170      * @return array
1171      * 
1172      * @todo should use generic mechanism to fetch setup related configs
1173      */
1174     protected function _getPasswordSettings()
1175     {
1176         $configs = array(
1177             Tinebase_Config::PASSWORD_CHANGE                     => 1,
1178             Tinebase_Config::PASSWORD_POLICY_ACTIVE              => 0,
1179             Tinebase_Config::PASSWORD_POLICY_ONLYASCII           => 0,
1180             Tinebase_Config::PASSWORD_POLICY_MIN_LENGTH          => 0,
1181             Tinebase_Config::PASSWORD_POLICY_MIN_WORD_CHARS      => 0,
1182             Tinebase_Config::PASSWORD_POLICY_MIN_UPPERCASE_CHARS => 0,
1183             Tinebase_Config::PASSWORD_POLICY_MIN_SPECIAL_CHARS   => 0,
1184             Tinebase_Config::PASSWORD_POLICY_MIN_NUMBERS         => 0,
1185         );
1186
1187         $result = array();
1188         $tinebaseInstalled = $this->isInstalled('Tinebase');
1189         foreach ($configs as $config => $default) {
1190             $result[$config] = ($tinebaseInstalled) ? Tinebase_Config::getInstance()->get($config, $default) : $default;
1191         }
1192         
1193         return $result;
1194     }
1195     
1196     /**
1197      * get Reuse Username to login textbox
1198      * 
1199      * @return array
1200      * 
1201      * @todo should use generic mechanism to fetch setup related configs
1202      */
1203     protected function _getReuseUsernameSettings()
1204     {
1205         $configs = array(
1206             Tinebase_Config::REUSEUSERNAME_SAVEUSERNAME         => 0,
1207         );
1208
1209         $result = array();
1210         $tinebaseInstalled = $this->isInstalled('Tinebase');
1211         foreach ($configs as $config => $default) {
1212             $result[$config] = ($tinebaseInstalled) ? Tinebase_Config::getInstance()->get($config, $default) : $default;
1213         }
1214         
1215         return $result;
1216     }
1217     
1218     /**
1219      * get email config
1220      *
1221      * @return array
1222      */
1223     public function getEmailConfig()
1224     {
1225         $result = array();
1226         
1227         foreach ($this->_emailConfigKeys as $configName => $configKey) {
1228             $config = Tinebase_Config::getInstance()->get($configKey, new Tinebase_Config_Struct(array()))->toArray();
1229             if (! empty($config) && ! isset($config['active'])) {
1230                 $config['active'] = TRUE;
1231             }
1232             $result[$configName] = $config;
1233         }
1234         
1235         return $result;
1236     }
1237     
1238     /**
1239      * save email config
1240      *
1241      * @param array $_data
1242      * @return void
1243      */
1244     public function saveEmailConfig($_data)
1245     {
1246         // this is a dangerous TRACE as there might be passwords in here!
1247         //if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_data, TRUE));
1248         
1249         $this->_enableCaching();
1250         
1251         foreach ($this->_emailConfigKeys as $configName => $configKey) {
1252             if ((isset($_data[$configName]) || array_key_exists($configName, $_data))) {
1253                 // fetch current config first and preserve all values that aren't in $_data array
1254                 $currentConfig = Tinebase_Config::getInstance()->get($configKey, new Tinebase_Config_Struct(array()))->toArray();
1255                 $newConfig = array_merge($_data[$configName], array_diff_key($currentConfig, $_data[$configName]));
1256                 Tinebase_Config::getInstance()->set($configKey, $newConfig);
1257             }
1258         }
1259     }
1260     
1261     /**
1262      * returns all email config keys
1263      *
1264      * @return array
1265      */
1266     public function getEmailConfigKeys()
1267     {
1268         return $this->_emailConfigKeys;
1269     }
1270     
1271     /**
1272      * get accepted terms config
1273      *
1274      * @return integer
1275      */
1276     public function getAcceptedTerms()
1277     {
1278         return Tinebase_Config::getInstance()->get(Tinebase_Config::ACCEPTEDTERMSVERSION, 0);
1279     }
1280     
1281     /**
1282      * save acceptedTermsVersion
1283      *
1284      * @param $_data
1285      * @return void
1286      */
1287     public function saveAcceptedTerms($_data)
1288     {
1289         Tinebase_Config::getInstance()->set(Tinebase_Config::ACCEPTEDTERMSVERSION, $_data);
1290     }
1291     
1292     /**
1293      * save config option in db
1294      *
1295      * @param string $key
1296      * @param string|array $value
1297      * @param string $applicationName
1298      * @return void
1299      */
1300     public function setConfigOption($key, $value, $applicationName = 'Tinebase')
1301     {
1302         $config = Tinebase_Config_Abstract::factory($applicationName);
1303         
1304         if ($config) {
1305             $config->set($key, $value);
1306         }
1307     }
1308     
1309     /**
1310      * create new setup user session
1311      *
1312      * @param   string $_username
1313      * @param   string $_password
1314      * @return  bool
1315      */
1316     public function login($_username, $_password)
1317     {
1318         $setupAuth = new Setup_Auth($_username, $_password);
1319         $authResult = Zend_Auth::getInstance()->authenticate($setupAuth);
1320         
1321         if ($authResult->isValid()) {
1322             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Valid credentials, setting username in session and registry.');
1323             Tinebase_Session::regenerateId();
1324             
1325             Setup_Core::set(Setup_Core::USER, $_username);
1326             Setup_Session::getSessionNamespace()->setupuser = $_username;
1327             return true;
1328             
1329         } else {
1330             Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Invalid credentials! ' . print_r($authResult->getMessages(), TRUE));
1331             Tinebase_Session::expireSessionCookie();
1332             sleep(2);
1333             return false;
1334         }
1335     }
1336     
1337     /**
1338      * destroy session
1339      *
1340      * @return void
1341      */
1342     public function logout()
1343     {
1344         $_SESSION = array();
1345         
1346         Tinebase_Session::destroyAndRemoveCookie();
1347     }
1348     
1349     /**
1350      * install list of applications
1351      *
1352      * @param array $_applications list of application names
1353      * @param array | optional $_options
1354      * @return void
1355      */
1356     public function installApplications($_applications, $_options = null)
1357     {
1358         $this->_clearCache();
1359         
1360         // check requirements for initial install / add required apps to list
1361         if (! $this->isInstalled('Tinebase')) {
1362     
1363             $minimumRequirements = array('Tinebase', 'Addressbook', 'Admin');
1364             
1365             foreach ($minimumRequirements as $requiredApp) {
1366                 if (!in_array($requiredApp, $_applications) && !$this->isInstalled($requiredApp)) {
1367                     // Addressbook has to be installed with Tinebase for initial data (user contact)
1368                     Setup_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
1369                         . ' ' . $requiredApp . ' has to be installed first (adding it to list).'
1370                     );
1371                     $_applications[] = $requiredApp;
1372                 }
1373             }
1374         }
1375         
1376         // get xml and sort apps first
1377         $applications = array();
1378         foreach($_applications as $applicationName) {
1379             if ($this->isInstalled($applicationName)) {
1380                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " skipping installation of application {$applicationName} because it is already installed");
1381             } else {
1382                 $applications[$applicationName] = $this->getSetupXml($applicationName);
1383             }
1384         }
1385         $applications = $this->_sortInstallableApplications($applications);
1386         
1387         Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Installing applications: ' . print_r(array_keys($applications), true));
1388         
1389         foreach ($applications as $name => $xml) {
1390             $this->_installApplication($xml, $_options);
1391         }
1392     }
1393
1394     /**
1395      * delete list of applications
1396      *
1397      * @param array $_applications list of application names
1398      */
1399     public function uninstallApplications($_applications)
1400     {
1401         $this->_clearCache();
1402
1403         $installedApps = Tinebase_Application::getInstance()->getApplications();
1404         
1405         // uninstall all apps if tinebase ist going to be uninstalled
1406         if (count($installedApps) !== count($_applications) && in_array('Tinebase', $_applications)) {
1407             $_applications = $installedApps->name;
1408         }
1409         
1410         // deactivate foreign key check if all installed apps should be uninstalled
1411         if (count($installedApps) == count($_applications) && get_class($this->_backend) == 'Setup_Backend_Mysql') {
1412             $this->_backend->setForeignKeyChecks(0);
1413             foreach ($installedApps as $app) {
1414                 if ($app->name != 'Tinebase') {
1415                     $this->_uninstallApplication($app);
1416                 } else {
1417                     $tinebase = $app;
1418                 }
1419             }
1420             // tinebase should be uninstalled last
1421             $this->_uninstallApplication($tinebase);
1422             $this->_backend->setForeignKeyChecks(1);
1423         } else {
1424             // get xml and sort apps first
1425             $applications = array();
1426             foreach($_applications as $applicationName) {
1427                 $applications[$applicationName] = $this->getSetupXml($applicationName);
1428             }
1429             $applications = $this->_sortUninstallableApplications($applications);
1430             
1431             foreach ($applications as $name => $xml) {
1432                 $app = Tinebase_Application::getInstance()->getApplicationByName($name);
1433                 $this->_uninstallApplication($app);
1434             }
1435         }
1436     }
1437     
1438     /**
1439      * install given application
1440      *
1441      * @param  SimpleXMLElement $_xml
1442      * @param  array | optional $_options
1443      * @return void
1444      * @throws Tinebase_Exception_Backend_Database
1445      */
1446     protected function _installApplication(SimpleXMLElement $_xml, $_options = null)
1447     {
1448         if ($this->_backend === NULL) {
1449             throw new Tinebase_Exception_Backend_Database('Need configured and working database backend for install.');
1450         }
1451         
1452         try {
1453             $createdTables = array();
1454             if (isset($_xml->tables)) {
1455                 foreach ($_xml->tables[0] as $tableXML) {
1456                     $table = Setup_Backend_Schema_Table_Factory::factory('Xml', $tableXML);
1457                     $currentTable = $table->name;
1458                     
1459                     try {
1460                         $this->_backend->createTable($table);
1461                     } catch (Zend_Db_Statement_Exception $zdse) {
1462                         throw new Tinebase_Exception_Backend_Database('Could not create table: ' . $zdse->getMessage());
1463                     } catch (Zend_Db_Adapter_Exception $zdae) {
1464                         throw new Tinebase_Exception_Backend_Database('Could not create table: ' . $zdae->getMessage());
1465                     }
1466                     $createdTables[] = $table;
1467                 }
1468             }
1469     
1470             $application = new Tinebase_Model_Application(array(
1471                 'name'      => (string)$_xml->name,
1472                 'status'    => $_xml->status ? (string)$_xml->status : Tinebase_Application::ENABLED,
1473                 'order'     => $_xml->order ? (string)$_xml->order : 99,
1474                 'version'   => (string)$_xml->version
1475             ));
1476             
1477             Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' installing application: ' . $_xml->name);
1478             
1479             $application = Tinebase_Application::getInstance()->addApplication($application);
1480             
1481             // keep track of tables belonging to this application
1482             foreach ($createdTables as $table) {
1483                 Tinebase_Application::getInstance()->addApplicationTable($application, (string) $table->name, (int) $table->version);
1484             }
1485             
1486             // insert default records
1487             if (isset($_xml->defaultRecords)) {
1488                 foreach ($_xml->defaultRecords[0] as $record) {
1489                     $this->_backend->execInsertStatement($record);
1490                 }
1491             }
1492             
1493             // look for import definitions and put them into the db
1494             $this->createImportExportDefinitions($application);
1495             
1496             Setup_Initialize::initialize($application, $_options);
1497         } catch (Exception $e) {
1498             $table = (isset($currentTable)) ? ' Table: ' . $currentTable : '';
1499             Setup_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' error at installing: ' . $_xml->name . $table . ' Exception: ' . $e->getMessage() . ' Trace: ' . $e->getTraceAsString());
1500             throw $e;
1501         }
1502     }
1503
1504     /**
1505      * look for import definitions and put them into the db
1506      *
1507      * @param Tinebase_Model_Application $_application
1508      */
1509     public function createImportExportDefinitions($_application)
1510     {
1511         foreach (array('Import', 'Export') as $type) {
1512             $path =
1513                 $this->_baseDir . $_application->name .
1514                 DIRECTORY_SEPARATOR . $type . DIRECTORY_SEPARATOR . 'definitions';
1515     
1516             if (file_exists($path)) {
1517                 foreach (new DirectoryIterator($path) as $item) {
1518                     $filename = $path . DIRECTORY_SEPARATOR . $item->getFileName();
1519                     if (preg_match("/\.xml/", $filename)) {
1520                         try {
1521                             Tinebase_ImportExportDefinition::getInstance()->updateOrCreateFromFilename($filename, $_application);
1522                         } catch (Exception $e) {
1523                             Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
1524                                 . ' Not installing import/export definion from file: ' . $filename
1525                                 . ' / Error message: ' . $e->getMessage());
1526                         }
1527                     }
1528                 }
1529             }
1530         }
1531     }
1532     
1533     /**
1534      * uninstall app
1535      *
1536      * @param Tinebase_Model_Application $_application
1537      * @throws Setup_Exception
1538      */
1539     protected function _uninstallApplication(Tinebase_Model_Application $_application)
1540     {
1541         if ($this->_backend === null) {
1542             throw new Setup_Exception('No setup backend available');
1543         }
1544         
1545         Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Uninstall ' . $_application);
1546         try {
1547             $applicationTables = Tinebase_Application::getInstance()->getApplicationTables($_application);
1548         } catch (Zend_Db_Statement_Exception $zdse) {
1549             Setup_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . " " . $zdse);
1550             throw new Setup_Exception('Could not uninstall ' . $_application . ' (you might need to remove the tables by yourself): ' . $zdse->getMessage());
1551         }
1552         $disabledFK = FALSE;
1553         $db = Tinebase_Core::getDb();
1554         
1555         do {
1556             $oldCount = count($applicationTables);
1557
1558             if ($_application->name == 'Tinebase') {
1559                 $installedApplications = Tinebase_Application::getInstance()->getApplications(NULL, 'id');
1560                 if (count($installedApplications) !== 1) {
1561                     throw new Setup_Exception_Dependency('Failed to uninstall application "Tinebase" because of dependencies to other installed applications.');
1562                 }
1563             }
1564
1565             foreach ($applicationTables as $key => $table) {
1566                 Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " Remove table: $table");
1567                 
1568                 try {
1569                     // drop foreign keys which point to current table first
1570                     $foreignKeys = $this->_backend->getExistingForeignKeys($table);
1571                     foreach ($foreignKeys as $foreignKey) {
1572                         Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . 
1573                             " Drop index: " . $foreignKey['table_name'] . ' => ' . $foreignKey['constraint_name']);
1574                         $this->_backend->dropForeignKey($foreignKey['table_name'], $foreignKey['constraint_name']);
1575                     }
1576                     
1577                     // drop table
1578                     $this->_backend->dropTable($table);
1579                     
1580                     if ($_application->name != 'Tinebase') {
1581                         Tinebase_Application::getInstance()->removeApplicationTable($_application, $table);
1582                     }
1583                     
1584                     unset($applicationTables[$key]);
1585                     
1586                 } catch (Zend_Db_Statement_Exception $e) {
1587                     // we need to catch exceptions here, as we don't want to break here, as a table
1588                     // might still have some foreign keys
1589                     // this works with mysql only
1590                     $message = $e->getMessage();
1591                     Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . " Could not drop table $table - " . $message);
1592                     
1593                     // remove app table if table not found in db
1594                     if (preg_match('/SQLSTATE\[42S02\]: Base table or view not found/', $message) && $_application->name != 'Tinebase') {
1595                         Tinebase_Application::getInstance()->removeApplicationTable($_application, $table);
1596                         unset($applicationTables[$key]);
1597                     } else {
1598                         Setup_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . " Disabling foreign key checks ... ");
1599                         if ($db instanceof Zend_Db_Adapter_Pdo_Mysql) {
1600                             $db->query("SET FOREIGN_KEY_CHECKS=0");
1601                         }
1602                         $disabledFK = TRUE;
1603                     }
1604                 }
1605             }
1606             
1607             if ($oldCount > 0 && count($applicationTables) == $oldCount) {
1608                 throw new Setup_Exception('dead lock detected oldCount: ' . $oldCount);
1609             }
1610         } while (count($applicationTables) > 0);
1611         
1612         if ($disabledFK) {
1613             if ($db instanceof Zend_Db_Adapter_Pdo_Mysql) {
1614                 Setup_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . " Enabling foreign key checks again... ");
1615                 $db->query("SET FOREIGN_KEY_CHECKS=1");
1616             }
1617         }
1618         
1619         if ($_application->name != 'Tinebase') {
1620             // delete containers, config options and other data for app
1621             Tinebase_Application::getInstance()->removeApplicationData($_application);
1622             
1623             // remove application from table of installed applications
1624             Tinebase_Application::getInstance()->deleteApplication($_application);
1625         }
1626         Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " Removed app: " . $_application->name);
1627     }
1628
1629     /**
1630      * sort applications by checking dependencies
1631      *
1632      * @param array $_applications
1633      * @return array
1634      */
1635     protected function _sortInstallableApplications($_applications)
1636     {
1637         $result = array();
1638         
1639         // begin with Tinebase, Admin and Addressbook
1640         $alwaysOnTop = array('Tinebase', 'Admin', 'Addressbook');
1641         foreach ($alwaysOnTop as $app) {
1642             if (isset($_applications[$app])) {
1643                 $result[$app] = $_applications[$app];
1644                 unset($_applications[$app]);
1645             }
1646         }
1647         
1648         // get all apps to install ($name => $dependencies)
1649         $appsToSort = array();
1650         foreach($_applications as $name => $xml) {
1651             $depends = (array) $xml->depends;
1652             if (isset($depends['application'])) {
1653                 if ($depends['application'] == 'Tinebase') {
1654                     $appsToSort[$name] = array();
1655                     
1656                 } else {
1657                     $depends['application'] = (array) $depends['application'];
1658                     
1659                     foreach ($depends['application'] as $app) {
1660                         // don't add tinebase (all apps depend on tinebase)
1661                         if ($app != 'Tinebase') {
1662                             $appsToSort[$name][] = $app;
1663                         }
1664                     }
1665                 }
1666             } else {
1667                 $appsToSort[$name] = array();
1668             }
1669         }
1670         
1671         //Setup_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($appsToSort, true));
1672         
1673         // re-sort apps
1674         $count = 0;
1675         while (count($appsToSort) > 0 && $count < MAXLOOPCOUNT) {
1676             
1677             foreach($appsToSort as $name => $depends) {
1678
1679                 if (empty($depends)) {
1680                     // no dependencies left -> copy app to result set
1681                     $result[$name] = $_applications[$name];
1682                     unset($appsToSort[$name]);
1683                 } else {
1684                     foreach ($depends as $key => $dependingAppName) {
1685                         if (in_array($dependingAppName, array_keys($result)) || $this->isInstalled($dependingAppName)) {
1686                             // remove from depending apps because it is already in result set
1687                             unset($appsToSort[$name][$key]);
1688                         }
1689                     }
1690                 }
1691             }
1692             $count++;
1693         }
1694         
1695         if ($count == MAXLOOPCOUNT) {
1696             Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
1697                 " Some Applications could not be installed because of (cyclic?) dependencies: " . print_r(array_keys($appsToSort), TRUE));
1698         }
1699         
1700         return $result;
1701     }
1702
1703     /**
1704      * sort applications by checking dependencies
1705      *
1706      * @param array $_applications
1707      * @return array
1708      */
1709     protected function _sortUninstallableApplications($_applications)
1710     {
1711         $result = array();
1712         
1713         // get all apps to uninstall ($name => $dependencies)
1714         $appsToSort = array();
1715         foreach($_applications as $name => $xml) {
1716             if ($name !== 'Tinebase') {
1717                 $depends = (array) $xml->depends;
1718                 if (isset($depends['application'])) {
1719                     if ($depends['application'] == 'Tinebase') {
1720                         $appsToSort[$name] = array();
1721                         
1722                     } else {
1723                         $depends['application'] = (array) $depends['application'];
1724                         
1725                         foreach ($depends['application'] as $app) {
1726                             // don't add tinebase (all apps depend on tinebase)
1727                             if ($app != 'Tinebase') {
1728                                 $appsToSort[$name][] = $app;
1729                             }
1730                         }
1731                     }
1732                 } else {
1733                     $appsToSort[$name] = array();
1734                 }
1735             }
1736         }
1737         
1738         // re-sort apps
1739         $count = 0;
1740         while (count($appsToSort) > 0 && $count < MAXLOOPCOUNT) {
1741
1742             foreach($appsToSort as $name => $depends) {
1743                 //Setup_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " - $count $name - " . print_r($depends, true));
1744                 
1745                 // don't uninstall if another app depends on this one
1746                 $otherAppDepends = FALSE;
1747                 foreach($appsToSort as $innerName => $innerDepends) {
1748                     if(in_array($name, $innerDepends)) {
1749                         $otherAppDepends = TRUE;
1750                         break;
1751                     }
1752                 }
1753                 
1754                 // add it to results
1755                 if (!$otherAppDepends) {
1756                     $result[$name] = $_applications[$name];
1757                     unset($appsToSort[$name]);
1758                 }
1759             }
1760             $count++;
1761         }
1762         
1763         if ($count == MAXLOOPCOUNT) {
1764             Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
1765                 " Some Applications could not be uninstalled because of (cyclic?) dependencies: " . print_r(array_keys($appsToSort), TRUE));
1766         }
1767
1768         // Tinebase is uninstalled last
1769         if (isset($_applications['Tinebase'])) {
1770             $result['Tinebase'] = $_applications['Tinebase'];
1771             unset($_applications['Tinebase']);
1772         }
1773         
1774         return $result;
1775     }
1776     
1777     /**
1778      * check if an application is installed
1779      *
1780      * @param string $appname
1781      * @return boolean
1782      */
1783     public function isInstalled($appname)
1784     {
1785         try {
1786             $result = Tinebase_Application::getInstance()->isInstalled($appname);
1787         } catch (Exception $e) {
1788             Setup_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Application ' . $appname . ' is not installed.');
1789             Setup_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $e);
1790             $result = FALSE;
1791         }
1792         
1793         return $result;
1794     }
1795     
1796     /**
1797      * clear cache
1798      *
1799      * @return void
1800      */
1801     protected function _clearCache()
1802     {
1803         // setup cache (via tinebase because it is disabled in setup by default)
1804         Tinebase_Core::setupCache(TRUE);
1805         
1806         Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Clearing cache ...');
1807         
1808         // clear cache
1809         $cache = Setup_Core::getCache()->clean(Zend_Cache::CLEANING_MODE_ALL);
1810         
1811         // deactivate cache again
1812         Tinebase_Core::setupCache(FALSE);
1813     }
1814     
1815     /**
1816      * returns TRUE if filesystem is available
1817      * 
1818      * @return boolean
1819      */
1820     public function isFilesystemAvailable()
1821     {
1822         if ($this->_isFileSystemAvailable === null) {
1823             try {
1824                 $session = Tinebase_Session::getSessionNamespace();
1825                 
1826                 if (isset($session->filesystemAvailable)) {
1827                     $this->_isFileSystemAvailable = $session->filesystemAvailable;
1828                     
1829                     return $this->_isFileSystemAvailable;
1830                 }
1831             } catch (Zend_Session_Exception $zse) {
1832                 $session = null;
1833             }
1834             
1835             $this->_isFileSystemAvailable = (!empty(Tinebase_Core::getConfig()->filesdir) && is_writeable(Tinebase_Core::getConfig()->filesdir));
1836             
1837             if ($session instanceof Zend_Session_Namespace) {
1838                 if (Tinebase_Session::isWritable()) {
1839                     $session->filesystemAvailable = $this->_isFileSystemAvailable;
1840                 }
1841             }
1842             
1843             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1844                 . ' Filesystem available: ' . ($this->_isFileSystemAvailable ? 'yes' : 'no'));
1845         }
1846         
1847         return $this->_isFileSystemAvailable;
1848     }
1849 }