11762 use doctrine for schema creation and update
[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://www.tine20.org/wiki/index.php/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) . '/Setup/setup.xml not found. If application got renamed or deleted, re-run setup.php.');
451         }
452         
453         $xml = simplexml_load_file($setupXML);
454
455         return $xml;
456     }
457     
458     /**
459      * check update
460      *
461      * @param   Tinebase_Model_Application $_application
462      * @throws  Setup_Exception
463      */
464     public function checkUpdate(Tinebase_Model_Application $_application)
465     {
466         $xmlTables = $this->getSetupXml($_application->name);
467         if(isset($xmlTables->tables)) {
468             foreach ($xmlTables->tables[0] as $tableXML) {
469                 $table = Setup_Backend_Schema_Table_Factory::factory('Xml', $tableXML);
470                 if (true == $this->_backend->tableExists($table->name)) {
471                     try {
472                         $this->_backend->checkTable($table);
473                     } catch (Setup_Exception $e) {
474                         Setup_Core::getLogger()->error(__METHOD__ . '::' . __LINE__ . " Checking table failed with message '{$e->getMessage()}'");
475                     }
476                 } else {
477                     throw new Setup_Exception('Table ' . $table->name . ' for application' . $_application->name . " does not exist. \n<strong>Update broken</strong>");
478                 }
479             }
480         }
481     }
482     
483     /**
484      * update installed application
485      *
486      * @param   Tinebase_Model_Application    $_application
487      * @param   string    $_majorVersion
488      * @return  array   messages
489      * @throws  Setup_Exception if current app version is too high
490      */
491     public function updateApplication(Tinebase_Model_Application $_application, $_majorVersion)
492     {
493         $setupXml = $this->getSetupXml($_application->name);
494         $messages = array();
495         
496         switch (version_compare($_application->version, $setupXml->version)) {
497             case -1:
498                 $message = "Executing updates for " . $_application->name . " (starting at " . $_application->version . ")";
499                 
500                 $messages[] = $message;
501                 Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' ' . $message);
502
503                 list($fromMajorVersion, $fromMinorVersion) = explode('.', $_application->version);
504         
505                 $minor = $fromMinorVersion;
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         
1059         $roles = Tinebase_Acl_Roles::getInstance();
1060         $roles->deleteAllRoles();
1061         
1062         // import users (from new backend) / create initial users (SQL)
1063         Tinebase_User::syncUsers(array('syncContactData' => TRUE));
1064         
1065         $roles->createInitialRoles();
1066         $applications = Tinebase_Application::getInstance()->getApplications(NULL, 'id');
1067         foreach ($applications as $application) {
1068              Setup_Initialize::initializeApplicationRights($application);
1069         }
1070     }
1071     
1072     /**
1073      * Update redirect settings
1074      *
1075      * @param array $_data
1076      * @return void
1077      */
1078     protected function _updateRedirectSettings($_data)
1079     {
1080         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_data, 1));
1081         $keys = array(Tinebase_Config::REDIRECTURL, Tinebase_Config::REDIRECTALWAYS, Tinebase_Config::REDIRECTTOREFERRER);
1082         foreach ($keys as $key) {
1083             if ((isset($_data[$key]) || array_key_exists($key, $_data))) {
1084                 if (strlen($_data[$key]) === 0) {
1085                     Tinebase_Config::getInstance()->delete($key);
1086                 } else {
1087                     Tinebase_Config::getInstance()->set($key, $_data[$key]);
1088                 }
1089             }
1090         }
1091     }
1092
1093         /**
1094      * update pw settings
1095      * 
1096      * @param array $data
1097      */
1098     protected function _updatePasswordSettings($data)
1099     {
1100         foreach ($data as $config => $value) {
1101             Tinebase_Config::getInstance()->set($config, $value);
1102         }
1103     }
1104     
1105     /**
1106      * update pw settings
1107      * 
1108      * @param array $data
1109      */
1110     protected function _updateReuseUsername($data)
1111     {
1112         foreach ($data as $config => $value) {
1113             Tinebase_Config::getInstance()->set($config, $value);
1114         }
1115     }
1116     
1117     /**
1118      *
1119      * get auth provider data
1120      *
1121      * @return array
1122      *
1123      * @todo get this from config table instead of file!
1124      */
1125     protected function _getAuthProviderData()
1126     {
1127         $result = Tinebase_Auth::getBackendConfigurationWithDefaults(Setup_Core::get(Setup_Core::CHECKDB));
1128         $result['backend'] = (Setup_Core::get(Setup_Core::CHECKDB)) ? Tinebase_Auth::getConfiguredBackend() : Tinebase_Auth::SQL;
1129
1130         return $result;
1131     }
1132     
1133     /**
1134      * get Accounts storage data
1135      *
1136      * @return array
1137      */
1138     protected function _getAccountsStorageData()
1139     {
1140         $result = Tinebase_User::getBackendConfigurationWithDefaults(Setup_Core::get(Setup_Core::CHECKDB));
1141         $result['backend'] = (Setup_Core::get(Setup_Core::CHECKDB)) ? Tinebase_User::getConfiguredBackend() : Tinebase_User::SQL;
1142
1143         return $result;
1144     }
1145     
1146     /**
1147      * Get redirect Settings from config table.
1148      * If Tinebase is not installed, default values will be returned.
1149      *
1150      * @return array
1151      */
1152     protected function _getRedirectSettings()
1153     {
1154         $return = array(
1155               Tinebase_Config::REDIRECTURL => '',
1156               Tinebase_Config::REDIRECTTOREFERRER => '0'
1157         );
1158         if (Setup_Core::get(Setup_Core::CHECKDB) && $this->isInstalled('Tinebase')) {
1159             $return[Tinebase_Config::REDIRECTURL] = Tinebase_Config::getInstance()->get(Tinebase_Config::REDIRECTURL, '');
1160             $return[Tinebase_Config::REDIRECTTOREFERRER] = Tinebase_Config::getInstance()->get(Tinebase_Config::REDIRECTTOREFERRER, '');
1161         }
1162         return $return;
1163     }
1164
1165     /**
1166      * get password settings
1167      * 
1168      * @return array
1169      * 
1170      * @todo should use generic mechanism to fetch setup related configs
1171      */
1172     protected function _getPasswordSettings()
1173     {
1174         $configs = array(
1175             Tinebase_Config::PASSWORD_CHANGE                     => 1,
1176             Tinebase_Config::PASSWORD_POLICY_ACTIVE              => 0,
1177             Tinebase_Config::PASSWORD_POLICY_ONLYASCII           => 0,
1178             Tinebase_Config::PASSWORD_POLICY_MIN_LENGTH          => 0,
1179             Tinebase_Config::PASSWORD_POLICY_MIN_WORD_CHARS      => 0,
1180             Tinebase_Config::PASSWORD_POLICY_MIN_UPPERCASE_CHARS => 0,
1181             Tinebase_Config::PASSWORD_POLICY_MIN_SPECIAL_CHARS   => 0,
1182             Tinebase_Config::PASSWORD_POLICY_MIN_NUMBERS         => 0,
1183         );
1184
1185         $result = array();
1186         $tinebaseInstalled = $this->isInstalled('Tinebase');
1187         foreach ($configs as $config => $default) {
1188             $result[$config] = ($tinebaseInstalled) ? Tinebase_Config::getInstance()->get($config, $default) : $default;
1189         }
1190         
1191         return $result;
1192     }
1193     
1194     /**
1195      * get Reuse Username to login textbox
1196      * 
1197      * @return array
1198      * 
1199      * @todo should use generic mechanism to fetch setup related configs
1200      */
1201     protected function _getReuseUsernameSettings()
1202     {
1203         $configs = array(
1204             Tinebase_Config::REUSEUSERNAME_SAVEUSERNAME         => 0,
1205         );
1206
1207         $result = array();
1208         $tinebaseInstalled = $this->isInstalled('Tinebase');
1209         foreach ($configs as $config => $default) {
1210             $result[$config] = ($tinebaseInstalled) ? Tinebase_Config::getInstance()->get($config, $default) : $default;
1211         }
1212         
1213         return $result;
1214     }
1215     
1216     /**
1217      * get email config
1218      *
1219      * @return array
1220      */
1221     public function getEmailConfig()
1222     {
1223         $result = array();
1224         
1225         foreach ($this->_emailConfigKeys as $configName => $configKey) {
1226             $config = Tinebase_Config::getInstance()->get($configKey, new Tinebase_Config_Struct(array()))->toArray();
1227             if (! empty($config) && ! isset($config['active'])) {
1228                 $config['active'] = TRUE;
1229             }
1230             $result[$configName] = $config;
1231         }
1232         
1233         return $result;
1234     }
1235     
1236     /**
1237      * save email config
1238      *
1239      * @param array $_data
1240      * @return void
1241      */
1242     public function saveEmailConfig($_data)
1243     {
1244         // this is a dangerous TRACE as there might be passwords in here!
1245         //if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_data, TRUE));
1246         
1247         $this->_enableCaching();
1248         
1249         foreach ($this->_emailConfigKeys as $configName => $configKey) {
1250             if ((isset($_data[$configName]) || array_key_exists($configName, $_data))) {
1251                 // fetch current config first and preserve all values that aren't in $_data array
1252                 $currentConfig = Tinebase_Config::getInstance()->get($configKey, new Tinebase_Config_Struct(array()))->toArray();
1253                 $newConfig = array_merge($_data[$configName], array_diff_key($currentConfig, $_data[$configName]));
1254                 Tinebase_Config::getInstance()->set($configKey, $newConfig);
1255             }
1256         }
1257     }
1258     
1259     /**
1260      * returns all email config keys
1261      *
1262      * @return array
1263      */
1264     public function getEmailConfigKeys()
1265     {
1266         return $this->_emailConfigKeys;
1267     }
1268     
1269     /**
1270      * get accepted terms config
1271      *
1272      * @return integer
1273      */
1274     public function getAcceptedTerms()
1275     {
1276         return Tinebase_Config::getInstance()->get(Tinebase_Config::ACCEPTEDTERMSVERSION, 0);
1277     }
1278     
1279     /**
1280      * save acceptedTermsVersion
1281      *
1282      * @param $_data
1283      * @return void
1284      */
1285     public function saveAcceptedTerms($_data)
1286     {
1287         Tinebase_Config::getInstance()->set(Tinebase_Config::ACCEPTEDTERMSVERSION, $_data);
1288     }
1289     
1290     /**
1291      * save config option in db
1292      *
1293      * @param string $key
1294      * @param string|array $value
1295      * @param string $applicationName
1296      * @return void
1297      */
1298     public function setConfigOption($key, $value, $applicationName = 'Tinebase')
1299     {
1300         $config = Tinebase_Config_Abstract::factory($applicationName);
1301         
1302         if ($config) {
1303             $config->set($key, $value);
1304         }
1305     }
1306     
1307     /**
1308      * create new setup user session
1309      *
1310      * @param   string $_username
1311      * @param   string $_password
1312      * @return  bool
1313      */
1314     public function login($_username, $_password)
1315     {
1316         $setupAuth = new Setup_Auth($_username, $_password);
1317         $authResult = Zend_Auth::getInstance()->authenticate($setupAuth);
1318         
1319         if ($authResult->isValid()) {
1320             Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Valid credentials, setting username in session and registry.');
1321             Tinebase_Session::regenerateId();
1322             
1323             Setup_Core::set(Setup_Core::USER, $_username);
1324             Setup_Session::getSessionNamespace()->setupuser = $_username;
1325             return true;
1326             
1327         } else {
1328             Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Invalid credentials! ' . print_r($authResult->getMessages(), TRUE));
1329             Tinebase_Session::expireSessionCookie();
1330             sleep(2);
1331             return false;
1332         }
1333     }
1334     
1335     /**
1336      * destroy session
1337      *
1338      * @return void
1339      */
1340     public function logout()
1341     {
1342         $_SESSION = array();
1343         
1344         Tinebase_Session::destroyAndRemoveCookie();
1345     }
1346     
1347     /**
1348      * install list of applications
1349      *
1350      * @param array $_applications list of application names
1351      * @param array | optional $_options
1352      * @return void
1353      */
1354     public function installApplications($_applications, $_options = null)
1355     {
1356         $this->_clearCache();
1357         
1358         // check requirements for initial install / add required apps to list
1359         if (! $this->isInstalled('Tinebase')) {
1360     
1361             $minimumRequirements = array('Addressbook', 'Tinebase', 'Admin');
1362             
1363             foreach ($minimumRequirements as $requiredApp) {
1364                 if (!in_array($requiredApp, $_applications) && !$this->isInstalled($requiredApp)) {
1365                     // Addressbook has to be installed with Tinebase for initial data (user contact)
1366                     Setup_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
1367                         . ' ' . $requiredApp . ' has to be installed first (adding it to list).'
1368                     );
1369                     $_applications[] = $requiredApp;
1370                 }
1371             }
1372         }
1373         
1374         // get xml and sort apps first
1375         $applications = array();
1376         foreach($_applications as $applicationName) {
1377             if ($this->isInstalled($applicationName)) {
1378                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " 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_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " Removed app: " . $_application->name);
1658     }
1659
1660     /**
1661      * sort applications by checking dependencies
1662      *
1663      * @param array $_applications
1664      * @return array
1665      */
1666     protected function _sortInstallableApplications($_applications)
1667     {
1668         $result = array();
1669         
1670         // begin with Tinebase, Admin and Addressbook
1671         $alwaysOnTop = array('Tinebase', 'Admin', 'Addressbook');
1672         foreach ($alwaysOnTop as $app) {
1673             if (isset($_applications[$app])) {
1674                 $result[$app] = $_applications[$app];
1675                 unset($_applications[$app]);
1676             }
1677         }
1678         
1679         // get all apps to install ($name => $dependencies)
1680         $appsToSort = array();
1681         foreach($_applications as $name => $xml) {
1682             $depends = (array) $xml->depends;
1683             if (isset($depends['application'])) {
1684                 if ($depends['application'] == 'Tinebase') {
1685                     $appsToSort[$name] = array();
1686                     
1687                 } else {
1688                     $depends['application'] = (array) $depends['application'];
1689                     
1690                     foreach ($depends['application'] as $app) {
1691                         // don't add tinebase (all apps depend on tinebase)
1692                         if ($app != 'Tinebase') {
1693                             $appsToSort[$name][] = $app;
1694                         }
1695                     }
1696                 }
1697             } else {
1698                 $appsToSort[$name] = array();
1699             }
1700         }
1701         
1702         //Setup_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($appsToSort, true));
1703         
1704         // re-sort apps
1705         $count = 0;
1706         while (count($appsToSort) > 0 && $count < MAXLOOPCOUNT) {
1707             
1708             foreach($appsToSort as $name => $depends) {
1709
1710                 if (empty($depends)) {
1711                     // no dependencies left -> copy app to result set
1712                     $result[$name] = $_applications[$name];
1713                     unset($appsToSort[$name]);
1714                 } else {
1715                     foreach ($depends as $key => $dependingAppName) {
1716                         if (in_array($dependingAppName, array_keys($result)) || $this->isInstalled($dependingAppName)) {
1717                             // remove from depending apps because it is already in result set
1718                             unset($appsToSort[$name][$key]);
1719                         }
1720                     }
1721                 }
1722             }
1723             $count++;
1724         }
1725         
1726         if ($count == MAXLOOPCOUNT) {
1727             Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
1728                 " Some Applications could not be installed because of (cyclic?) dependencies: " . print_r(array_keys($appsToSort), TRUE));
1729         }
1730         
1731         return $result;
1732     }
1733
1734     /**
1735      * sort applications by checking dependencies
1736      *
1737      * @param array $_applications
1738      * @return array
1739      */
1740     protected function _sortUninstallableApplications($_applications)
1741     {
1742         $result = array();
1743         
1744         // get all apps to uninstall ($name => $dependencies)
1745         $appsToSort = array();
1746         foreach($_applications as $name => $xml) {
1747             if ($name !== 'Tinebase') {
1748                 $depends = (array) $xml->depends;
1749                 if (isset($depends['application'])) {
1750                     if ($depends['application'] == 'Tinebase') {
1751                         $appsToSort[$name] = array();
1752                         
1753                     } else {
1754                         $depends['application'] = (array) $depends['application'];
1755                         
1756                         foreach ($depends['application'] as $app) {
1757                             // don't add tinebase (all apps depend on tinebase)
1758                             if ($app != 'Tinebase') {
1759                                 $appsToSort[$name][] = $app;
1760                             }
1761                         }
1762                     }
1763                 } else {
1764                     $appsToSort[$name] = array();
1765                 }
1766             }
1767         }
1768         
1769         // re-sort apps
1770         $count = 0;
1771         while (count($appsToSort) > 0 && $count < MAXLOOPCOUNT) {
1772
1773             foreach($appsToSort as $name => $depends) {
1774                 //Setup_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " - $count $name - " . print_r($depends, true));
1775                 
1776                 // don't uninstall if another app depends on this one
1777                 $otherAppDepends = FALSE;
1778                 foreach($appsToSort as $innerName => $innerDepends) {
1779                     if(in_array($name, $innerDepends)) {
1780                         $otherAppDepends = TRUE;
1781                         break;
1782                     }
1783                 }
1784                 
1785                 // add it to results
1786                 if (!$otherAppDepends) {
1787                     $result[$name] = $_applications[$name];
1788                     unset($appsToSort[$name]);
1789                 }
1790             }
1791             $count++;
1792         }
1793         
1794         if ($count == MAXLOOPCOUNT) {
1795             Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
1796                 " Some Applications could not be uninstalled because of (cyclic?) dependencies: " . print_r(array_keys($appsToSort), TRUE));
1797         }
1798
1799         // Tinebase is uninstalled last
1800         if (isset($_applications['Tinebase'])) {
1801             $result['Tinebase'] = $_applications['Tinebase'];
1802             unset($_applications['Tinebase']);
1803         }
1804         
1805         return $result;
1806     }
1807     
1808     /**
1809      * check if an application is installed
1810      *
1811      * @param string $appname
1812      * @return boolean
1813      */
1814     public function isInstalled($appname)
1815     {
1816         try {
1817             $result = Tinebase_Application::getInstance()->isInstalled($appname);
1818         } catch (Exception $e) {
1819             Setup_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Application ' . $appname . ' is not installed.');
1820             Setup_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $e);
1821             $result = FALSE;
1822         }
1823         
1824         return $result;
1825     }
1826     
1827     /**
1828      * clear cache
1829      *
1830      * @return void
1831      */
1832     protected function _clearCache()
1833     {
1834         // setup cache (via tinebase because it is disabled in setup by default)
1835         Tinebase_Core::setupCache(TRUE);
1836         
1837         Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Clearing cache ...');
1838         
1839         // clear cache
1840         $cache = Setup_Core::getCache()->clean(Zend_Cache::CLEANING_MODE_ALL);
1841         
1842         // deactivate cache again
1843         Tinebase_Core::setupCache(FALSE);
1844     }
1845
1846     /**
1847      * returns TRUE if filesystem is available
1848      * 
1849      * @return boolean
1850      */
1851     public function isFilesystemAvailable()
1852     {
1853         if ($this->_isFileSystemAvailable === null) {
1854             try {
1855                 $session = Tinebase_Session::getSessionNamespace();
1856
1857                 if (isset($session->filesystemAvailable)) {
1858                     $this->_isFileSystemAvailable = $session->filesystemAvailable;
1859
1860                     return $this->_isFileSystemAvailable;
1861                 }
1862             } catch (Zend_Session_Exception $zse) {
1863                 $session = null;
1864             }
1865
1866             $this->_isFileSystemAvailable = (!empty(Tinebase_Core::getConfig()->filesdir) && is_writeable(Tinebase_Core::getConfig()->filesdir));
1867
1868             if ($session instanceof Zend_Session_Namespace) {
1869                 if (Tinebase_Session::isWritable()) {
1870                     $session->filesystemAvailable = $this->_isFileSystemAvailable;
1871                 }
1872             }
1873
1874             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
1875                 . ' Filesystem available: ' . ($this->_isFileSystemAvailable ? 'yes' : 'no'));
1876         }
1877
1878         return $this->_isFileSystemAvailable;
1879     }
1880
1881     /**
1882      * backup
1883      *
1884      * @param $options array(
1885      *      'backupDir'  => string // where to store the backup
1886      *      'noTimestamp => bool   // don't append timestamp to backup dir
1887      *      'config'     => bool   // backup config
1888      *      'db'         => bool   // backup database
1889      *      'files'      => bool   // backup files
1890      *    )
1891      */
1892     public function backup($options)
1893     {
1894         $config = Setup_Core::getConfig();
1895
1896         $backupDir = isset($options['backupDir']) ? $options['backupDir'] : $config->backupDir;
1897         if (! $backupDir) {
1898             throw new Exception('backupDir not configured');
1899         }
1900
1901         if (! isset($options['noTimestamp'])) {
1902             $backupDir .= '/' . date_create('now', new DateTimeZone('UTC'))->format('Y-m-d-H-i-s');
1903         }
1904
1905         if (!is_dir($backupDir) && !mkdir($backupDir, 0700, true)) {
1906             throw new Exception("$backupDir could  not be created");
1907         }
1908
1909         if ($options['config']) {
1910             $configFile = stream_resolve_include_path('config.inc.php');
1911             $configDir = dirname($configFile);
1912
1913             $files = file_exists("$configDir/index.php") ? 'config.inc.php' : '.';
1914             `cd $configDir; tar cjf $backupDir/tine20_config.tar.bz2 $files`;
1915         }
1916
1917         if ($options['db']) {
1918             if (! $this->_backend) {
1919                 throw new Exception('db not configured, cannot backup');
1920             }
1921
1922             $backupOptions = array(
1923                 'backupDir'         => $backupDir,
1924                 'structTables'      => $this->_getBackupStructureOnlyTables(),
1925             );
1926
1927             $this->_backend->backup($backupOptions);
1928         }
1929
1930         $filesDir = isset($config->filesdir) ? $config->filesdir : false;
1931         if ($options['files'] && $filesDir) {
1932             `cd $filesDir; tar cjf $backupDir/tine20_files.tar.bz2 .`;
1933         }
1934     }
1935
1936     /**
1937      * returns an array of all tables of all applications that should only backup the structure
1938      *
1939      * @return array
1940      * @throws Setup_Exception_NotFound
1941      */
1942     protected function _getBackupStructureOnlyTables()
1943     {
1944         $tables = array();
1945
1946         // find tables that only backup structure
1947         $applications = Tinebase_Application::getInstance()->getApplications();
1948
1949         /**
1950          * @var $application Tinebase_Model_Application
1951          */
1952         foreach($applications as $application) {
1953             $tableDef = $this->getSetupXml($application->name);
1954             $structOnlys = $tableDef->xpath('//table/backupStructureOnly[text()="true"]');
1955
1956             foreach($structOnlys as $structOnly) {
1957                 $tableName = $structOnly->xpath('./../name/text()');
1958                 $tables[] = SQL_TABLE_PREFIX . $tableName[0];
1959             }
1960         }
1961
1962         return $tables;
1963     }
1964
1965     /**
1966      * restore
1967      *
1968      * @param $options array(
1969      *      'backupDir'  => string // location of backup to restore
1970      *      'config'     => bool   // restore config
1971      *      'db'         => bool   // restore database
1972      *      'files'      => bool   // restore files
1973      *    )
1974      *
1975      * @param $options
1976      * @throws Exception
1977      */
1978     public function restore($options)
1979     {
1980         if (! isset($options['backupDir'])) {
1981             throw new Exception("you need to specify the backupDir");
1982         }
1983
1984         if ($options['config']) {
1985             $configBackupFile = $options['backupDir']. '/tine20_config.tar.bz2';
1986             if (! file_exists($configBackupFile)) {
1987                 throw new Exception("$configBackupFile not found");
1988             }
1989
1990             $configDir = isset($options['configDir']) ? $options['configDir'] : false;
1991             if (!$configDir) {
1992                 $configFile = stream_resolve_include_path('config.inc.php');
1993                 if (!$configFile) {
1994                     throw new Exception("can't detect configDir, please use configDir option");
1995                 }
1996                 $configDir = dirname($configFile);
1997             }
1998
1999             `cd $configDir; tar xf $configBackupFile`;
2000         }
2001
2002         Setup_Core::setupConfig();
2003         $config = Setup_Core::getConfig();
2004
2005         if ($options['db']) {
2006             $this->_backend->restore($options['backupDir']);
2007         }
2008
2009         $filesDir = isset($config->filesdir) ? $config->filesdir : false;
2010         if ($options['files']) {
2011             $filesBackupFile = $options['backupDir'] . '/tine20_files.tar.bz2';
2012             if (! file_exists($filesBackupFile)) {
2013                 throw new Exception("$filesBackupFile not found");
2014             }
2015
2016             `cd $filesDir; tar xf $filesBackupFile`;
2017         }
2018     }
2019 }