4bd9768dc581812a8356bb48e1befb346eabf68c
[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
1469             else {
1470                 // create tables using doctrine 2
1471                 $application = Setup_Core::getApplicationInstance($_xml->name, '', true);
1472                 $models = $application->getModels(true /* MCv2only */);
1473                 Setup_SchemaTool::createSchema($_xml->name, $models);
1474
1475                 // adopt to old workflow
1476                 foreach($models as $model) {
1477                     $modelConfiguration = $model::getConfiguration();
1478                     $createdTables[] = (object) array(
1479                         'name' => Tinebase_Helper::array_value('name', $modelConfiguration->getTable()),
1480                         'version' => $modelConfiguration->getVersion(),
1481                     );
1482                 }
1483             }
1484     
1485             $application = new Tinebase_Model_Application(array(
1486                 'name'      => (string)$_xml->name,
1487                 'status'    => $_xml->status ? (string)$_xml->status : Tinebase_Application::ENABLED,
1488                 'order'     => $_xml->order ? (string)$_xml->order : 99,
1489                 'version'   => (string)$_xml->version
1490             ));
1491
1492             $application = Tinebase_Application::getInstance()->addApplication($application);
1493             
1494             // keep track of tables belonging to this application
1495             foreach ($createdTables as $table) {
1496                 Tinebase_Application::getInstance()->addApplicationTable($application, (string) $table->name, (int) $table->version);
1497             }
1498             
1499             // insert default records
1500             if (isset($_xml->defaultRecords)) {
1501                 foreach ($_xml->defaultRecords[0] as $record) {
1502                     $this->_backend->execInsertStatement($record);
1503                 }
1504             }
1505             
1506             // look for import definitions and put them into the db
1507             $this->createImportExportDefinitions($application);
1508             
1509             Setup_Initialize::initialize($application, $_options);
1510         } catch (Exception $e) {
1511             Tinebase_Exception::log($e, /* suppress trace */ false);
1512             throw $e;
1513         }
1514     }
1515
1516     protected function _createTable($table)
1517     {
1518         if (Setup_Core::isLogLevel(Zend_Log::DEBUG)) Setup_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Creating table: ' . $table->name);
1519
1520         try {
1521             $this->_backend->createTable($table);
1522         } catch (Zend_Db_Statement_Exception $zdse) {
1523             throw new Tinebase_Exception_Backend_Database('Could not create table: ' . $zdse->getMessage());
1524         } catch (Zend_Db_Adapter_Exception $zdae) {
1525             throw new Tinebase_Exception_Backend_Database('Could not create table: ' . $zdae->getMessage());
1526         }
1527     }
1528     /**
1529      * look for import definitions and put them into the db
1530      *
1531      * @param Tinebase_Model_Application $_application
1532      */
1533     public function createImportExportDefinitions($_application)
1534     {
1535         foreach (array('Import', 'Export') as $type) {
1536             $path =
1537                 $this->_baseDir . $_application->name .
1538                 DIRECTORY_SEPARATOR . $type . DIRECTORY_SEPARATOR . 'definitions';
1539     
1540             if (file_exists($path)) {
1541                 foreach (new DirectoryIterator($path) as $item) {
1542                     $filename = $path . DIRECTORY_SEPARATOR . $item->getFileName();
1543                     if (preg_match("/\.xml/", $filename)) {
1544                         try {
1545                             Tinebase_ImportExportDefinition::getInstance()->updateOrCreateFromFilename($filename, $_application);
1546                         } catch (Exception $e) {
1547                             Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
1548                                 . ' Not installing import/export definion from file: ' . $filename
1549                                 . ' / Error message: ' . $e->getMessage());
1550                         }
1551                     }
1552                 }
1553             }
1554         }
1555     }
1556     
1557     /**
1558      * uninstall app
1559      *
1560      * @param Tinebase_Model_Application $_application
1561      * @throws Setup_Exception
1562      */
1563     protected function _uninstallApplication(Tinebase_Model_Application $_application, $uninstallAll = false)
1564     {
1565         if ($this->_backend === null) {
1566             throw new Setup_Exception('No setup backend available');
1567         }
1568         
1569         Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Uninstall ' . $_application);
1570         try {
1571             $applicationTables = Tinebase_Application::getInstance()->getApplicationTables($_application);
1572         } catch (Zend_Db_Statement_Exception $zdse) {
1573             Setup_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . " " . $zdse);
1574             throw new Setup_Exception('Could not uninstall ' . $_application . ' (you might need to remove the tables by yourself): ' . $zdse->getMessage());
1575         }
1576         $disabledFK = FALSE;
1577         $db = Tinebase_Core::getDb();
1578         
1579         do {
1580             $oldCount = count($applicationTables);
1581
1582             if ($_application->name == 'Tinebase') {
1583                 $installedApplications = Tinebase_Application::getInstance()->getApplications(NULL, 'id');
1584                 if (count($installedApplications) !== 1) {
1585                     throw new Setup_Exception_Dependency('Failed to uninstall application "Tinebase" because of dependencies to other installed applications.');
1586                 }
1587             }
1588
1589             foreach ($applicationTables as $key => $table) {
1590                 Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " Remove table: $table");
1591                 
1592                 try {
1593                     // drop foreign keys which point to current table first
1594                     $foreignKeys = $this->_backend->getExistingForeignKeys($table);
1595                     foreach ($foreignKeys as $foreignKey) {
1596                         Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . 
1597                             " Drop index: " . $foreignKey['table_name'] . ' => ' . $foreignKey['constraint_name']);
1598                         $this->_backend->dropForeignKey($foreignKey['table_name'], $foreignKey['constraint_name']);
1599                     }
1600                     
1601                     // drop table
1602                     $this->_backend->dropTable($table);
1603                     
1604                     if ($_application->name != 'Tinebase') {
1605                         Tinebase_Application::getInstance()->removeApplicationTable($_application, $table);
1606                     }
1607                     
1608                     unset($applicationTables[$key]);
1609                     
1610                 } catch (Zend_Db_Statement_Exception $e) {
1611                     // we need to catch exceptions here, as we don't want to break here, as a table
1612                     // might still have some foreign keys
1613                     // this works with mysql only
1614                     $message = $e->getMessage();
1615                     Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . " Could not drop table $table - " . $message);
1616                     
1617                     // remove app table if table not found in db
1618                     if (preg_match('/SQLSTATE\[42S02\]: Base table or view not found/', $message) && $_application->name != 'Tinebase') {
1619                         Tinebase_Application::getInstance()->removeApplicationTable($_application, $table);
1620                         unset($applicationTables[$key]);
1621                     } else {
1622                         Setup_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . " Disabling foreign key checks ... ");
1623                         if ($db instanceof Zend_Db_Adapter_Pdo_Mysql) {
1624                             $db->query("SET FOREIGN_KEY_CHECKS=0");
1625                         }
1626                         $disabledFK = TRUE;
1627                     }
1628                 }
1629             }
1630             
1631             if ($oldCount > 0 && count($applicationTables) == $oldCount) {
1632                 throw new Setup_Exception('dead lock detected oldCount: ' . $oldCount);
1633             }
1634         } while (count($applicationTables) > 0);
1635         
1636         if ($disabledFK) {
1637             if ($db instanceof Zend_Db_Adapter_Pdo_Mysql) {
1638                 Setup_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . " Enabling foreign key checks again... ");
1639                 $db->query("SET FOREIGN_KEY_CHECKS=1");
1640             }
1641         }
1642         
1643         if ($_application->name != 'Tinebase') {
1644             if (!$uninstallAll) {
1645                 Tinebase_Relations::getInstance()->removeApplication($_application->name);
1646
1647                 Tinebase_Timemachine_ModificationLog::getInstance()->removeApplication($_application);
1648
1649                 // delete containers, config options and other data for app
1650                 Tinebase_Application::getInstance()->removeApplicationData($_application);
1651             }
1652             
1653             // remove application from table of installed applications
1654             Tinebase_Application::getInstance()->deleteApplication($_application);
1655         }
1656
1657         Setup_Uninitialize::uninitialize($_application);
1658
1659         Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " Removed app: " . $_application->name);
1660     }
1661
1662     /**
1663      * sort applications by checking dependencies
1664      *
1665      * @param array $_applications
1666      * @return array
1667      */
1668     protected function _sortInstallableApplications($_applications)
1669     {
1670         $result = array();
1671         
1672         // begin with Tinebase, Admin and Addressbook
1673         $alwaysOnTop = array('Tinebase', 'Admin', 'Addressbook');
1674         foreach ($alwaysOnTop as $app) {
1675             if (isset($_applications[$app])) {
1676                 $result[$app] = $_applications[$app];
1677                 unset($_applications[$app]);
1678             }
1679         }
1680         
1681         // get all apps to install ($name => $dependencies)
1682         $appsToSort = array();
1683         foreach($_applications as $name => $xml) {
1684             $depends = (array) $xml->depends;
1685             if (isset($depends['application'])) {
1686                 if ($depends['application'] == 'Tinebase') {
1687                     $appsToSort[$name] = array();
1688                     
1689                 } else {
1690                     $depends['application'] = (array) $depends['application'];
1691                     
1692                     foreach ($depends['application'] as $app) {
1693                         // don't add tinebase (all apps depend on tinebase)
1694                         if ($app != 'Tinebase') {
1695                             $appsToSort[$name][] = $app;
1696                         }
1697                     }
1698                 }
1699             } else {
1700                 $appsToSort[$name] = array();
1701             }
1702         }
1703         
1704         //Setup_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($appsToSort, true));
1705         
1706         // re-sort apps
1707         $count = 0;
1708         while (count($appsToSort) > 0 && $count < MAXLOOPCOUNT) {
1709             
1710             foreach($appsToSort as $name => $depends) {
1711
1712                 if (empty($depends)) {
1713                     // no dependencies left -> copy app to result set
1714                     $result[$name] = $_applications[$name];
1715                     unset($appsToSort[$name]);
1716                 } else {
1717                     foreach ($depends as $key => $dependingAppName) {
1718                         if (in_array($dependingAppName, array_keys($result)) || $this->isInstalled($dependingAppName)) {
1719                             // remove from depending apps because it is already in result set
1720                             unset($appsToSort[$name][$key]);
1721                         }
1722                     }
1723                 }
1724             }
1725             $count++;
1726         }
1727         
1728         if ($count == MAXLOOPCOUNT) {
1729             Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
1730                 " Some Applications could not be installed because of (cyclic?) dependencies: " . print_r(array_keys($appsToSort), TRUE));
1731         }
1732         
1733         return $result;
1734     }
1735
1736     /**
1737      * sort applications by checking dependencies
1738      *
1739      * @param array $_applications
1740      * @return array
1741      */
1742     protected function _sortUninstallableApplications($_applications)
1743     {
1744         $result = array();
1745         
1746         // get all apps to uninstall ($name => $dependencies)
1747         $appsToSort = array();
1748         foreach($_applications as $name => $xml) {
1749             if ($name !== 'Tinebase') {
1750                 $depends = (array) $xml->depends;
1751                 if (isset($depends['application'])) {
1752                     if ($depends['application'] == 'Tinebase') {
1753                         $appsToSort[$name] = array();
1754                         
1755                     } else {
1756                         $depends['application'] = (array) $depends['application'];
1757                         
1758                         foreach ($depends['application'] as $app) {
1759                             // don't add tinebase (all apps depend on tinebase)
1760                             if ($app != 'Tinebase') {
1761                                 $appsToSort[$name][] = $app;
1762                             }
1763                         }
1764                     }
1765                 } else {
1766                     $appsToSort[$name] = array();
1767                 }
1768             }
1769         }
1770         
1771         // re-sort apps
1772         $count = 0;
1773         while (count($appsToSort) > 0 && $count < MAXLOOPCOUNT) {
1774
1775             foreach($appsToSort as $name => $depends) {
1776                 //Setup_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " - $count $name - " . print_r($depends, true));
1777                 
1778                 // don't uninstall if another app depends on this one
1779                 $otherAppDepends = FALSE;
1780                 foreach($appsToSort as $innerName => $innerDepends) {
1781                     if(in_array($name, $innerDepends)) {
1782                         $otherAppDepends = TRUE;
1783                         break;
1784                     }
1785                 }
1786                 
1787                 // add it to results
1788                 if (!$otherAppDepends) {
1789                     $result[$name] = $_applications[$name];
1790                     unset($appsToSort[$name]);
1791                 }
1792             }
1793             $count++;
1794         }
1795         
1796         if ($count == MAXLOOPCOUNT) {
1797             Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
1798                 " Some Applications could not be uninstalled because of (cyclic?) dependencies: " . print_r(array_keys($appsToSort), TRUE));
1799         }
1800
1801         // Tinebase is uninstalled last
1802         if (isset($_applications['Tinebase'])) {
1803             $result['Tinebase'] = $_applications['Tinebase'];
1804             unset($_applications['Tinebase']);
1805         }
1806         
1807         return $result;
1808     }
1809     
1810     /**
1811      * check if an application is installed
1812      *
1813      * @param string $appname
1814      * @return boolean
1815      */
1816     public function isInstalled($appname)
1817     {
1818         try {
1819             $result = Tinebase_Application::getInstance()->isInstalled($appname);
1820         } catch (Exception $e) {
1821             Setup_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Application ' . $appname . ' is not installed.');
1822             Setup_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $e);
1823             $result = FALSE;
1824         }
1825         
1826         return $result;
1827     }
1828     
1829     /**
1830      * clear cache
1831      *
1832      * @return void
1833      */
1834     protected function _clearCache()
1835     {
1836         // setup cache (via tinebase because it is disabled in setup by default)
1837         Tinebase_Core::setupCache(TRUE);
1838         
1839         Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Clearing cache ...');
1840         
1841         // clear cache
1842         $cache = Setup_Core::getCache()->clean(Zend_Cache::CLEANING_MODE_ALL);
1843         
1844         // deactivate cache again
1845         Tinebase_Core::setupCache(FALSE);
1846     }
1847
1848     /**
1849      * returns TRUE if filesystem is available
1850      * 
1851      * @return boolean
1852      */
1853     public function isFilesystemAvailable()
1854     {
1855         if ($this->_isFileSystemAvailable === null) {
1856             try {
1857                 $session = Tinebase_Session::getSessionNamespace();
1858
1859                 if (isset($session->filesystemAvailable)) {
1860                     $this->_isFileSystemAvailable = $session->filesystemAvailable;
1861
1862                     return $this->_isFileSystemAvailable;
1863                 }
1864             } catch (Zend_Session_Exception $zse) {
1865                 $session = null;
1866             }
1867
1868             $this->_isFileSystemAvailable = (!empty(Tinebase_Core::getConfig()->filesdir) && is_writeable(Tinebase_Core::getConfig()->filesdir));
1869
1870             if ($session instanceof Zend_Session_Namespace) {
1871                 if (Tinebase_Session::isWritable()) {
1872                     $session->filesystemAvailable = $this->_isFileSystemAvailable;
1873                 }
1874             }
1875
1876             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1877                 . ' Filesystem available: ' . ($this->_isFileSystemAvailable ? 'yes' : 'no'));
1878         }
1879
1880         return $this->_isFileSystemAvailable;
1881     }
1882
1883     /**
1884      * backup
1885      *
1886      * @param $options array(
1887      *      'backupDir'  => string // where to store the backup
1888      *      'noTimestamp => bool   // don't append timestamp to backup dir
1889      *      'config'     => bool   // backup config
1890      *      'db'         => bool   // backup database
1891      *      'files'      => bool   // backup files
1892      *    )
1893      */
1894     public function backup($options)
1895     {
1896         $config = Setup_Core::getConfig();
1897
1898         $backupDir = isset($options['backupDir']) ? $options['backupDir'] : $config->backupDir;
1899         if (! $backupDir) {
1900             throw new Exception('backupDir not configured');
1901         }
1902
1903         if (! isset($options['noTimestamp'])) {
1904             $backupDir .= '/' . date_create('now', new DateTimeZone('UTC'))->format('Y-m-d-H-i-s');
1905         }
1906
1907         if (!is_dir($backupDir) && !mkdir($backupDir, 0700, true)) {
1908             throw new Exception("$backupDir could  not be created");
1909         }
1910
1911         if ($options['config']) {
1912             $configFile = stream_resolve_include_path('config.inc.php');
1913             $configDir = dirname($configFile);
1914
1915             $files = file_exists("$configDir/index.php") ? 'config.inc.php' : '.';
1916             `cd $configDir; tar cjf $backupDir/tine20_config.tar.bz2 $files`;
1917         }
1918
1919         if ($options['db']) {
1920             if (! $this->_backend) {
1921                 throw new Exception('db not configured, cannot backup');
1922             }
1923
1924             $backupOptions = array(
1925                 'backupDir'         => $backupDir,
1926                 'structTables'      => $this->_getBackupStructureOnlyTables(),
1927             );
1928
1929             $this->_backend->backup($backupOptions);
1930         }
1931
1932         $filesDir = isset($config->filesdir) ? $config->filesdir : false;
1933         if ($options['files'] && $filesDir) {
1934             `cd $filesDir; tar cjf $backupDir/tine20_files.tar.bz2 .`;
1935         }
1936     }
1937
1938     /**
1939      * returns an array of all tables of all applications that should only backup the structure
1940      *
1941      * @return array
1942      * @throws Setup_Exception_NotFound
1943      */
1944     protected function _getBackupStructureOnlyTables()
1945     {
1946         $tables = array();
1947
1948         // find tables that only backup structure
1949         $applications = Tinebase_Application::getInstance()->getApplications();
1950
1951         /**
1952          * @var $application Tinebase_Model_Application
1953          */
1954         foreach($applications as $application) {
1955             $tableDef = $this->getSetupXml($application->name);
1956             $structOnlys = $tableDef->xpath('//table/backupStructureOnly[text()="true"]');
1957
1958             foreach($structOnlys as $structOnly) {
1959                 $tableName = $structOnly->xpath('./../name/text()');
1960                 $tables[] = SQL_TABLE_PREFIX . $tableName[0];
1961             }
1962         }
1963
1964         return $tables;
1965     }
1966
1967     /**
1968      * restore
1969      *
1970      * @param $options array(
1971      *      'backupDir'  => string // location of backup to restore
1972      *      'config'     => bool   // restore config
1973      *      'db'         => bool   // restore database
1974      *      'files'      => bool   // restore files
1975      *    )
1976      *
1977      * @param $options
1978      * @throws Exception
1979      */
1980     public function restore($options)
1981     {
1982         if (! isset($options['backupDir'])) {
1983             throw new Exception("you need to specify the backupDir");
1984         }
1985
1986         if ($options['config']) {
1987             $configBackupFile = $options['backupDir']. '/tine20_config.tar.bz2';
1988             if (! file_exists($configBackupFile)) {
1989                 throw new Exception("$configBackupFile not found");
1990             }
1991
1992             $configDir = isset($options['configDir']) ? $options['configDir'] : false;
1993             if (!$configDir) {
1994                 $configFile = stream_resolve_include_path('config.inc.php');
1995                 if (!$configFile) {
1996                     throw new Exception("can't detect configDir, please use configDir option");
1997                 }
1998                 $configDir = dirname($configFile);
1999             }
2000
2001             `cd $configDir; tar xf $configBackupFile`;
2002         }
2003
2004         Setup_Core::setupConfig();
2005         $config = Setup_Core::getConfig();
2006
2007         if ($options['db']) {
2008             $this->_backend->restore($options['backupDir']);
2009         }
2010
2011         $filesDir = isset($config->filesdir) ? $config->filesdir : false;
2012         if ($options['files']) {
2013             $filesBackupFile = $options['backupDir'] . '/tine20_files.tar.bz2';
2014             if (! file_exists($filesBackupFile)) {
2015                 throw new Exception("$filesBackupFile not found");
2016             }
2017
2018             `cd $filesDir; tar xf $filesBackupFile`;
2019         }
2020     }
2021 }