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