0013250: setup.php --install should only install available apps on demand
[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             throw new Setup_Exception('Tinebase already installed!');
1449         }
1450
1451         $mysqlBackupFile = $options['backupDir'] . '/tine20_mysql.sql.bz2';
1452         if (! file_exists($mysqlBackupFile)) {
1453             throw new Setup_Exception("$mysqlBackupFile not found");
1454         }
1455
1456         Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Installing from dump ' . $mysqlBackupFile);
1457
1458         $this->_replaceTinebaseidInDump($mysqlBackupFile);
1459         $this->restore($options);
1460
1461         $setupUser = Setup_Update_Abstract::getSetupFromConfigOrCreateOnTheFly();
1462         if ($setupUser && ! Tinebase_Core::getUser() instanceof Tinebase_Model_User) {
1463             Tinebase_Core::set(Tinebase_Core::USER, $setupUser);
1464         }
1465
1466         // set the replication master id
1467         $tinebase = Tinebase_Application::getInstance()->getApplicationByName('Tinebase');
1468         $state = $tinebase->state;
1469         if (!is_array($state)) {
1470             $state = array();
1471         }
1472         $state[Tinebase_Model_Application::STATE_REPLICATION_MASTER_ID] = Tinebase_Timemachine_ModificationLog::getInstance()->getMaxInstanceSeq();
1473         $tinebase->state = $state;
1474         Tinebase_Application::getInstance()->updateApplication($tinebase);
1475
1476         $this->updateApplications();
1477
1478         return true;
1479     }
1480
1481     /**
1482      * replace old Tinebase ID in dump to make sure we have a unique installation ID
1483      *
1484      * TODO: think about moving the Tinebase ID (and more info) to a metadata.json file in the backup zip
1485      *
1486      * @param $mysqlBackupFile
1487      * @throws Setup_Exception
1488      */
1489     protected function _replaceTinebaseidInDump($mysqlBackupFile)
1490     {
1491         // fetch old Tinebase ID
1492         $cmd = "bzcat $mysqlBackupFile | grep \",'Tinebase',\"";
1493         $result = exec($cmd);
1494         if (! preg_match("/'([0-9a-f]+)','Tinebase'/", $result, $matches)) {
1495             throw new Setup_Exception('could not find Tinebase ID in dump');
1496         }
1497         $oldTinebaseId = $matches[1];
1498
1499         $cmd = "bzcat $mysqlBackupFile | sed s/"
1500             . $oldTinebaseId . '/'
1501             . Tinebase_Record_Abstract::generateUID() . "/g | " // g for global!
1502             . "bzip2 > " . $mysqlBackupFile . '.tmp';
1503
1504         Setup_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . $cmd);
1505
1506         exec($cmd);
1507         copy($mysqlBackupFile . '.tmp', $mysqlBackupFile);
1508         unlink($mysqlBackupFile . '.tmp');
1509     }
1510
1511     /**
1512      * delete list of applications
1513      *
1514      * @param array $_applications list of application names
1515      */
1516     public function uninstallApplications($_applications)
1517     {
1518         if (null === ($user = Setup_Update_Abstract::getSetupFromConfigOrCreateOnTheFly())) {
1519             throw new Tinebase_Exception('could not create setup user');
1520         }
1521         Tinebase_Core::set(Tinebase_Core::USER, $user);
1522
1523         $this->_clearCache();
1524
1525         $installedApps = Tinebase_Application::getInstance()->getApplications();
1526         
1527         // uninstall all apps if tinebase ist going to be uninstalled
1528         if (count($installedApps) !== count($_applications) && in_array('Tinebase', $_applications)) {
1529             $_applications = $installedApps->name;
1530         }
1531         
1532         // deactivate foreign key check if all installed apps should be uninstalled
1533         $deactivatedForeignKeyCheck = false;
1534         if (count($installedApps) == count($_applications) && get_class($this->_backend) == 'Setup_Backend_Mysql') {
1535             $this->_backend->setForeignKeyChecks(0);
1536             $deactivatedForeignKeyCheck = true;
1537         }
1538
1539         // get xml and sort apps first
1540         $applications = array();
1541         foreach ($_applications as $applicationName) {
1542             try {
1543                 $applications[$applicationName] = $this->getSetupXml($applicationName);
1544             } catch (Setup_Exception_NotFound $senf) {
1545                 // application setup.xml not found
1546                 Tinebase_Exception::log($senf);
1547                 $applications[$applicationName] = null;
1548             }
1549         }
1550         $applications = $this->_sortUninstallableApplications($applications);
1551
1552         foreach ($applications as $name => $xml) {
1553             $app = Tinebase_Application::getInstance()->getApplicationByName($name);
1554             $this->_uninstallApplication($app);
1555         }
1556
1557         if (true === $deactivatedForeignKeyCheck) {
1558             $this->_backend->setForeignKeyChecks(1);
1559         }
1560     }
1561     
1562     /**
1563      * install given application
1564      *
1565      * @param  SimpleXMLElement $_xml
1566      * @param  array | optional $_options
1567      * @return void
1568      * @throws Tinebase_Exception_Backend_Database
1569      */
1570     protected function _installApplication(SimpleXMLElement $_xml, $_options = null)
1571     {
1572         if ($this->_backend === NULL) {
1573             throw new Tinebase_Exception_Backend_Database('Need configured and working database backend for install.');
1574         }
1575         
1576         if (!$this->checkDatabasePrefix()) {
1577             throw new Tinebase_Exception_Backend_Database('Tableprefix is too long');
1578         }
1579         
1580         try {
1581             if (Setup_Core::isLogLevel(Zend_Log::INFO)) Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Installing application: ' . $_xml->name);
1582
1583             $createdTables = array();
1584
1585             // traditional xml declaration
1586             if (isset($_xml->tables)) {
1587                 foreach ($_xml->tables[0] as $tableXML) {
1588                     $table = Setup_Backend_Schema_Table_Factory::factory('Xml', $tableXML);
1589                     if ($this->_createTable($table) !== true) {
1590                         // table was gracefully not created, maybe due to missing requirements, just continue
1591                         continue;
1592                     }
1593                     $createdTables[] = $table;
1594                 }
1595             }
1596
1597             // do we have modelconfig + doctrine
1598             else {
1599                 $application = Setup_Core::getApplicationInstance($_xml->name, '', true);
1600                 $models = $application->getModels(true /* MCv2only */);
1601
1602                 if (count($models) > 0) {
1603                     // create tables using doctrine 2
1604                     Setup_SchemaTool::createSchema($_xml->name, $models);
1605
1606                     // adopt to old workflow
1607                     foreach ($models as $model) {
1608                         $modelConfiguration = $model::getConfiguration();
1609                         $createdTables[] = (object)array(
1610                             'name' => Tinebase_Helper::array_value('name', $modelConfiguration->getTable()),
1611                             'version' => $modelConfiguration->getVersion(),
1612                         );
1613                     }
1614                 }
1615             }
1616     
1617             $application = new Tinebase_Model_Application(array(
1618                 'name'      => (string)$_xml->name,
1619                 'status'    => $_xml->status ? (string)$_xml->status : Tinebase_Application::ENABLED,
1620                 'order'     => $_xml->order ? (string)$_xml->order : 99,
1621                 'version'   => (string)$_xml->version
1622             ));
1623
1624             $application = Tinebase_Application::getInstance()->addApplication($application);
1625             
1626             // keep track of tables belonging to this application
1627             foreach ($createdTables as $table) {
1628                 Tinebase_Application::getInstance()->addApplicationTable($application, (string) $table->name, (int) $table->version);
1629             }
1630             
1631             // insert default records
1632             if (isset($_xml->defaultRecords)) {
1633                 foreach ($_xml->defaultRecords[0] as $record) {
1634                     $this->_backend->execInsertStatement($record);
1635                 }
1636             }
1637             
1638             Setup_Initialize::initialize($application, $_options);
1639
1640             // look for import definitions and put them into the db
1641             $this->createImportExportDefinitions($application);
1642         } catch (Exception $e) {
1643             Tinebase_Exception::log($e, /* suppress trace */ false);
1644             throw $e;
1645         }
1646     }
1647
1648     protected function _createTable($table)
1649     {
1650         if (Setup_Core::isLogLevel(Zend_Log::DEBUG)) Setup_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Creating table: ' . $table->name);
1651
1652         try {
1653             $result = $this->_backend->createTable($table);
1654         } catch (Zend_Db_Statement_Exception $zdse) {
1655             throw new Tinebase_Exception_Backend_Database('Could not create table: ' . $zdse->getMessage());
1656         } catch (Zend_Db_Adapter_Exception $zdae) {
1657             throw new Tinebase_Exception_Backend_Database('Could not create table: ' . $zdae->getMessage());
1658         }
1659
1660         return $result;
1661     }
1662
1663     /**
1664      * look for export & import definitions and put them into the db
1665      *
1666      * @param Tinebase_Model_Application $_application
1667      */
1668     public function createImportExportDefinitions($_application)
1669     {
1670         foreach (array('Import', 'Export') as $type) {
1671             $path =
1672                 $this->_baseDir . $_application->name .
1673                 DIRECTORY_SEPARATOR . $type . DIRECTORY_SEPARATOR . 'definitions';
1674     
1675             if (file_exists($path)) {
1676                 foreach (new DirectoryIterator($path) as $item) {
1677                     $filename = $path . DIRECTORY_SEPARATOR . $item->getFileName();
1678                     if (preg_match("/\.xml/", $filename)) {
1679                         try {
1680                             Tinebase_ImportExportDefinition::getInstance()->updateOrCreateFromFilename($filename, $_application);
1681                         } catch (Exception $e) {
1682                             Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
1683                                 . ' Not installing import/export definion from file: ' . $filename
1684                                 . ' / Error message: ' . $e->getMessage());
1685                         }
1686                     }
1687                 }
1688             }
1689
1690             $path =
1691                 $this->_baseDir . $_application->name .
1692                 DIRECTORY_SEPARATOR . $type . DIRECTORY_SEPARATOR . 'templates';
1693
1694             if (file_exists($path)) {
1695                 $fileSystem = Tinebase_FileSystem::getInstance();
1696
1697                 $basepath = $fileSystem->getApplicationBasePath(
1698                     'Tinebase',
1699                     Tinebase_FileSystem::FOLDER_TYPE_SHARED
1700                 ) . '/' . strtolower($type);
1701
1702                 if (false === $fileSystem->isDir($basepath)) {
1703                     $fileSystem->createAclNode($basepath);
1704                 }
1705
1706                 $templateAppPath = Tinebase_Model_Tree_Node_Path::createFromPath($basepath . '/templates/' . $_application->name);
1707
1708                 if (! $fileSystem->isDir($templateAppPath->statpath)) {
1709                     $fileSystem->mkdir($templateAppPath->statpath);
1710                 }
1711
1712                 foreach (new DirectoryIterator($path) as $item) {
1713                     if (!$item->isFile()) {
1714                         continue;
1715                     }
1716                     if (false === ($content = file_get_contents($item->getPathname()))) {
1717                         Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
1718                             . ' Could not import template: ' . $item->getPathname());
1719                         continue;
1720                     }
1721                     if (false === ($file = $fileSystem->fopen($templateAppPath->statpath . '/' . $item->getFileName(), 'w'))) {
1722                         Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
1723                             . ' could not open ' . $templateAppPath->statpath . '/' . $item->getFileName() . ' for writting');
1724                         continue;
1725                     }
1726                     fwrite($file, $content);
1727                     if (true !== $fileSystem->fclose($file)) {
1728                         Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
1729                             . ' write to ' . $templateAppPath->statpath . '/' . $item->getFileName() . ' did not succeed');
1730                         continue;
1731                     }
1732                 }
1733             }
1734         }
1735     }
1736     
1737     /**
1738      * uninstall app
1739      *
1740      * @param Tinebase_Model_Application $_application
1741      * @throws Setup_Exception
1742      */
1743     protected function _uninstallApplication(Tinebase_Model_Application $_application, $uninstallAll = false)
1744     {
1745         if ($this->_backend === null) {
1746             throw new Setup_Exception('No setup backend available');
1747         }
1748         
1749         Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Uninstall ' . $_application);
1750         try {
1751             $applicationTables = Tinebase_Application::getInstance()->getApplicationTables($_application);
1752         } catch (Zend_Db_Statement_Exception $zdse) {
1753             Setup_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . " " . $zdse);
1754             throw new Setup_Exception('Could not uninstall ' . $_application . ' (you might need to remove the tables by yourself): ' . $zdse->getMessage());
1755         }
1756         $disabledFK = FALSE;
1757         $db = Tinebase_Core::getDb();
1758         
1759         do {
1760             $oldCount = count($applicationTables);
1761
1762             if ($_application->name == 'Tinebase') {
1763                 $installedApplications = Tinebase_Application::getInstance()->getApplications(NULL, 'id');
1764                 if (count($installedApplications) !== 1) {
1765                     Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Installed apps: ' . print_r($installedApplications->name, true));
1766                     throw new Setup_Exception_Dependency('Failed to uninstall application "Tinebase" because of dependencies to other installed applications.');
1767                 }
1768             }
1769
1770             foreach ($applicationTables as $key => $table) {
1771                 Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " Remove table: $table");
1772                 
1773                 try {
1774                     // drop foreign keys which point to current table first
1775                     $foreignKeys = $this->_backend->getExistingForeignKeys($table);
1776                     foreach ($foreignKeys as $foreignKey) {
1777                         Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . 
1778                             " Drop index: " . $foreignKey['table_name'] . ' => ' . $foreignKey['constraint_name']);
1779                         $this->_backend->dropForeignKey($foreignKey['table_name'], $foreignKey['constraint_name']);
1780                     }
1781                     
1782                     // drop table
1783                     $this->_backend->dropTable($table);
1784                     
1785                     if ($_application->name != 'Tinebase') {
1786                         Tinebase_Application::getInstance()->removeApplicationTable($_application, $table);
1787                     }
1788                     
1789                     unset($applicationTables[$key]);
1790                     
1791                 } catch (Zend_Db_Statement_Exception $e) {
1792                     // we need to catch exceptions here, as we don't want to break here, as a table
1793                     // might still have some foreign keys
1794                     // this works with mysql only
1795                     $message = $e->getMessage();
1796                     Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . " Could not drop table $table - " . $message);
1797                     
1798                     // remove app table if table not found in db
1799                     if (preg_match('/SQLSTATE\[42S02\]: Base table or view not found/', $message) && $_application->name != 'Tinebase') {
1800                         Tinebase_Application::getInstance()->removeApplicationTable($_application, $table);
1801                         unset($applicationTables[$key]);
1802                     } else {
1803                         Setup_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . " Disabling foreign key checks ... ");
1804                         if ($db instanceof Zend_Db_Adapter_Pdo_Mysql) {
1805                             $db->query("SET FOREIGN_KEY_CHECKS=0");
1806                         }
1807                         $disabledFK = TRUE;
1808                     }
1809                 }
1810             }
1811             
1812             if ($oldCount > 0 && count($applicationTables) == $oldCount) {
1813                 throw new Setup_Exception('dead lock detected oldCount: ' . $oldCount);
1814             }
1815         } while (count($applicationTables) > 0);
1816         
1817         if ($disabledFK) {
1818             if ($db instanceof Zend_Db_Adapter_Pdo_Mysql) {
1819                 Setup_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . " Enabling foreign key checks again... ");
1820                 $db->query("SET FOREIGN_KEY_CHECKS=1");
1821             }
1822         }
1823         
1824         if ($_application->name != 'Tinebase') {
1825             if (!$uninstallAll) {
1826                 Tinebase_Relations::getInstance()->removeApplication($_application->name);
1827
1828                 Tinebase_Timemachine_ModificationLog::getInstance()->removeApplication($_application);
1829
1830                 // delete containers, config options and other data for app
1831                 Tinebase_Application::getInstance()->removeApplicationAuxiliaryData($_application);
1832             }
1833             
1834             // remove application from table of installed applications
1835             Tinebase_Application::getInstance()->deleteApplication($_application);
1836         }
1837
1838         Setup_Uninitialize::uninitialize($_application);
1839
1840         Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " Removed app: " . $_application->name);
1841     }
1842
1843     /**
1844      * sort applications by checking dependencies
1845      *
1846      * @param array $_applications
1847      * @return array
1848      */
1849     protected function _sortInstallableApplications($_applications)
1850     {
1851         $result = array();
1852         
1853         // begin with Tinebase, Admin and Addressbook
1854         $alwaysOnTop = array('Tinebase', 'Admin', 'Addressbook');
1855         foreach ($alwaysOnTop as $app) {
1856             if (isset($_applications[$app])) {
1857                 $result[$app] = $_applications[$app];
1858                 unset($_applications[$app]);
1859             }
1860         }
1861         
1862         // get all apps to install ($name => $dependencies)
1863         $appsToSort = array();
1864         foreach($_applications as $name => $xml) {
1865             $depends = (array) $xml->depends;
1866             if (isset($depends['application'])) {
1867                 if ($depends['application'] == 'Tinebase') {
1868                     $appsToSort[$name] = array();
1869                     
1870                 } else {
1871                     $depends['application'] = (array) $depends['application'];
1872                     
1873                     foreach ($depends['application'] as $app) {
1874                         // don't add tinebase (all apps depend on tinebase)
1875                         if ($app != 'Tinebase') {
1876                             $appsToSort[$name][] = $app;
1877                         }
1878                     }
1879                 }
1880             } else {
1881                 $appsToSort[$name] = array();
1882             }
1883         }
1884         
1885         //Setup_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($appsToSort, true));
1886         
1887         // re-sort apps
1888         $count = 0;
1889         while (count($appsToSort) > 0 && $count < MAXLOOPCOUNT) {
1890             
1891             foreach($appsToSort as $name => $depends) {
1892
1893                 if (empty($depends)) {
1894                     // no dependencies left -> copy app to result set
1895                     $result[$name] = $_applications[$name];
1896                     unset($appsToSort[$name]);
1897                 } else {
1898                     foreach ($depends as $key => $dependingAppName) {
1899                         if (in_array($dependingAppName, array_keys($result)) || $this->isInstalled($dependingAppName)) {
1900                             // remove from depending apps because it is already in result set
1901                             unset($appsToSort[$name][$key]);
1902                         }
1903                     }
1904                 }
1905             }
1906             $count++;
1907         }
1908         
1909         if ($count == MAXLOOPCOUNT) {
1910             Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
1911                 " Some Applications could not be installed because of (cyclic?) dependencies: " . print_r(array_keys($appsToSort), TRUE));
1912         }
1913         
1914         return $result;
1915     }
1916
1917     /**
1918      * sort applications by checking dependencies
1919      *
1920      * @param array $_applications
1921      * @return array
1922      */
1923     protected function _sortUninstallableApplications($_applications)
1924     {
1925         $result = array();
1926         
1927         // get all apps to uninstall ($name => $dependencies)
1928         $appsToSort = array();
1929         foreach($_applications as $name => $xml) {
1930             if ($name !== 'Tinebase') {
1931                 $depends = $xml ? (array) $xml->depends : array();
1932                 if (isset($depends['application'])) {
1933                     if ($depends['application'] == 'Tinebase') {
1934                         $appsToSort[$name] = array();
1935                         
1936                     } else {
1937                         $depends['application'] = (array) $depends['application'];
1938                         
1939                         foreach ($depends['application'] as $app) {
1940                             // don't add tinebase (all apps depend on tinebase)
1941                             if ($app != 'Tinebase') {
1942                                 $appsToSort[$name][] = $app;
1943                             }
1944                         }
1945                     }
1946                 } else {
1947                     $appsToSort[$name] = array();
1948                 }
1949             }
1950         }
1951         
1952         // re-sort apps
1953         $count = 0;
1954         while (count($appsToSort) > 0 && $count < MAXLOOPCOUNT) {
1955
1956             foreach($appsToSort as $name => $depends) {
1957                 //Setup_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " - $count $name - " . print_r($depends, true));
1958                 
1959                 // don't uninstall if another app depends on this one
1960                 $otherAppDepends = FALSE;
1961                 foreach($appsToSort as $innerName => $innerDepends) {
1962                     if(in_array($name, $innerDepends)) {
1963                         $otherAppDepends = TRUE;
1964                         break;
1965                     }
1966                 }
1967                 
1968                 // add it to results
1969                 if (!$otherAppDepends) {
1970                     $result[$name] = $_applications[$name];
1971                     unset($appsToSort[$name]);
1972                 }
1973             }
1974             $count++;
1975         }
1976         
1977         if ($count == MAXLOOPCOUNT) {
1978             Setup_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ .
1979                 " Some Applications could not be uninstalled because of (cyclic?) dependencies: " . print_r(array_keys($appsToSort), TRUE));
1980         }
1981
1982         // Tinebase is uninstalled last
1983         if (isset($_applications['Tinebase'])) {
1984             $result['Tinebase'] = $_applications['Tinebase'];
1985             unset($_applications['Tinebase']);
1986         }
1987         
1988         return $result;
1989     }
1990     
1991     /**
1992      * check if an application is installed
1993      *
1994      * @param string $appname
1995      * @return boolean
1996      */
1997     public function isInstalled($appname)
1998     {
1999         try {
2000             $result = Tinebase_Application::getInstance()->isInstalled($appname);
2001         } catch (Exception $e) {
2002             Setup_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Application ' . $appname . ' is not installed.');
2003             Setup_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . $e);
2004             $result = FALSE;
2005         }
2006         
2007         return $result;
2008     }
2009     
2010     /**
2011      * clear cache
2012      *
2013      * @return void
2014      */
2015     protected function _clearCache()
2016     {
2017         // setup cache (via tinebase because it is disabled in setup by default)
2018         Tinebase_Core::setupCache(TRUE);
2019         
2020         Setup_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Clearing cache ...');
2021         
2022         // clear cache
2023         $cache = Setup_Core::getCache()->clean(Zend_Cache::CLEANING_MODE_ALL);
2024
2025         Tinebase_Application::getInstance()->resetClassCache();
2026         Tinebase_Cache_PerRequest::getInstance()->reset();
2027
2028         // deactivate cache again
2029         Tinebase_Core::setupCache(FALSE);
2030     }
2031
2032     /**
2033      * returns TRUE if filesystem is available
2034      * 
2035      * @return boolean
2036      */
2037     public function isFilesystemAvailable()
2038     {
2039         if ($this->_isFileSystemAvailable === null) {
2040             try {
2041                 $session = Tinebase_Session::getSessionNamespace();
2042
2043                 if (isset($session->filesystemAvailable)) {
2044                     $this->_isFileSystemAvailable = $session->filesystemAvailable;
2045
2046                     return $this->_isFileSystemAvailable;
2047                 }
2048             } catch (Zend_Session_Exception $zse) {
2049                 $session = null;
2050             }
2051
2052             $this->_isFileSystemAvailable = (!empty(Tinebase_Core::getConfig()->filesdir) && is_writeable(Tinebase_Core::getConfig()->filesdir));
2053
2054             if ($session instanceof Zend_Session_Namespace) {
2055                 if (Tinebase_Session::isWritable()) {
2056                     $session->filesystemAvailable = $this->_isFileSystemAvailable;
2057                 }
2058             }
2059
2060             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
2061                 . ' Filesystem available: ' . ($this->_isFileSystemAvailable ? 'yes' : 'no'));
2062         }
2063
2064         return $this->_isFileSystemAvailable;
2065     }
2066
2067     /**
2068      * backup
2069      *
2070      * @param $options array(
2071      *      'backupDir'  => string // where to store the backup
2072      *      'noTimestamp => bool   // don't append timestamp to backup dir
2073      *      'config'     => bool   // backup config
2074      *      'db'         => bool   // backup database
2075      *      'files'      => bool   // backup files
2076      *    )
2077      */
2078     public function backup($options)
2079     {
2080         $config = Setup_Core::getConfig();
2081
2082         $backupDir = isset($options['backupDir']) ? $options['backupDir'] : $config->backupDir;
2083         if (! $backupDir) {
2084             throw new Exception('backupDir not configured');
2085         }
2086
2087         if (! isset($options['noTimestamp'])) {
2088             $backupDir .= '/' . date_create('now', new DateTimeZone('UTC'))->format('Y-m-d-H-i-s');
2089         }
2090
2091         if (!is_dir($backupDir) && !mkdir($backupDir, 0700, true)) {
2092             throw new Exception("$backupDir could  not be created");
2093         }
2094
2095         if ($options['config']) {
2096             $configFile = stream_resolve_include_path('config.inc.php');
2097             $configDir = dirname($configFile);
2098
2099             $files = file_exists("$configDir/index.php") ? 'config.inc.php' : '.';
2100             `cd $configDir; tar cjf $backupDir/tine20_config.tar.bz2 $files`;
2101         }
2102
2103         if ($options['db']) {
2104             if (! $this->_backend) {
2105                 throw new Exception('db not configured, cannot backup');
2106             }
2107
2108             $backupOptions = array(
2109                 'backupDir'         => $backupDir,
2110                 'structTables'      => $this->_getBackupStructureOnlyTables(),
2111             );
2112
2113             $this->_backend->backup($backupOptions);
2114         }
2115
2116         $filesDir = isset($config->filesdir) ? $config->filesdir : false;
2117         if ($options['files'] && $filesDir) {
2118             `cd $filesDir; tar cjf $backupDir/tine20_files.tar.bz2 .`;
2119         }
2120     }
2121
2122     /**
2123      * returns an array of all tables of all applications that should only backup the structure
2124      *
2125      * @return array
2126      * @throws Setup_Exception_NotFound
2127      */
2128     protected function _getBackupStructureOnlyTables()
2129     {
2130         $tables = array();
2131
2132         // find tables that only backup structure
2133         $applications = Tinebase_Application::getInstance()->getApplications();
2134
2135         /**
2136          * @var $application Tinebase_Model_Application
2137          */
2138         foreach($applications as $application) {
2139             $tableDef = $this->getSetupXml($application->name);
2140             $structOnlys = $tableDef->xpath('//table/backupStructureOnly[text()="true"]');
2141
2142             foreach($structOnlys as $structOnly) {
2143                 $tableName = $structOnly->xpath('./../name/text()');
2144                 $tables[] = SQL_TABLE_PREFIX . $tableName[0];
2145             }
2146         }
2147
2148         return $tables;
2149     }
2150
2151     /**
2152      * restore
2153      *
2154      * @param $options array(
2155      *      'backupDir'  => string // location of backup to restore
2156      *      'config'     => bool   // restore config
2157      *      'db'         => bool   // restore database
2158      *      'files'      => bool   // restore files
2159      *    )
2160      *
2161      * @param $options
2162      * @throws Setup_Exception
2163      */
2164     public function restore($options)
2165     {
2166         if (! isset($options['backupDir'])) {
2167             throw new Setup_Exception("you need to specify the backupDir");
2168         }
2169
2170         if (isset($options['config']) && $options['config']) {
2171             $configBackupFile = $options['backupDir']. '/tine20_config.tar.bz2';
2172             if (! file_exists($configBackupFile)) {
2173                 throw new Setup_Exception("$configBackupFile not found");
2174             }
2175
2176             $configDir = isset($options['configDir']) ? $options['configDir'] : false;
2177             if (!$configDir) {
2178                 $configFile = stream_resolve_include_path('config.inc.php');
2179                 if (!$configFile) {
2180                     throw new Setup_Exception("can't detect configDir, please use configDir option");
2181                 }
2182                 $configDir = dirname($configFile);
2183             }
2184
2185             `cd $configDir; tar xf $configBackupFile`;
2186         }
2187
2188         Setup_Core::setupConfig();
2189         $config = Setup_Core::getConfig();
2190
2191         if (isset($options['db']) && $options['db']) {
2192             $this->_backend->restore($options['backupDir']);
2193         }
2194
2195         $filesDir = isset($config->filesdir) ? $config->filesdir : false;
2196         if (isset($options['files']) && $options['files']) {
2197             $dir = $options['backupDir'];
2198             $filesBackupFile = $dir . '/tine20_files.tar.bz2';
2199             if (! file_exists($filesBackupFile)) {
2200                 throw new Setup_Exception("$filesBackupFile not found");
2201             }
2202
2203             `cd $filesDir; tar xf $filesBackupFile`;
2204         }
2205     }
2206
2207     public function compareSchema($options)
2208     {
2209         if (! isset($options['otherdb'])) {
2210             throw new Exception("you need to specify the otherdb");
2211         }
2212
2213         return Setup_SchemaTool::compareSchema($options['otherdb']);
2214     }
2215 }