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