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