f4980dc6008f2feb81287708fbae956d525a9528
[tine20] / tine20 / Tinebase / Config / Abstract.php
1 <?php
2 /**
3  * @package     Tinebase
4  * @subpackage  Config
5  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
6  * @copyright   Copyright (c) 2007-2015 Metaways Infosystems GmbH (http://www.metaways.de)
7  * @author      Cornelius Weiss <c.weiss@metaways.de>
8  */
9
10 /**
11  * base for config classes
12  * 
13  * @package     Tinebase
14  * @subpackage  Config
15  * 
16  * @todo support protected function interceptor for get property: _get<PropertyName>(&$data)
17  * @todo support protected function interceptor for set property: _set<PropertyName>(&$data)
18  * @todo update db to json encode all configs
19  * @todo support array collections definitions
20  */
21 abstract class Tinebase_Config_Abstract
22 {
23     /**
24      * object config type
25      * 
26      * @var string
27      */
28     const TYPE_OBJECT = 'object';
29
30     /**
31      * integer config type
32      * 
33      * @var string
34      */
35     const TYPE_INT = 'int';
36     
37     /**
38      * boolean config type
39      * 
40      * @var string
41      */
42     const TYPE_BOOL = 'bool';
43     
44     /**
45      * string config type
46      * 
47      * @var string
48      */
49     const TYPE_STRING = 'string';
50     
51     /**
52      * float config type
53      * 
54      * @var string
55      */
56     const TYPE_FLOAT = 'float';
57     
58     /**
59      * dateTime config type
60      * 
61      * @var string
62      */
63     const TYPE_DATETIME = 'dateTime';
64     
65     /**
66      * keyFieldConfig config type
67      * 
68      * @var string
69      */
70     const TYPE_KEYFIELD = 'keyFieldConfig';
71     
72     /**
73      * config key for enabled features / feature switch
74      *
75      * @var string
76      */
77     const ENABLED_FEATURES = 'features';
78     
79     /**
80      * application name this config belongs to
81      *
82      * @var string
83      */
84     protected $_appName;
85     
86     /**
87      * config file data.
88      * 
89      * @var array
90      */
91     private static $_configFileData;
92     
93     /**
94      * application defaults config file data.
95      * 
96      * @var array
97      */
98     private static $_appDefaultsConfigFileData;
99     
100     /**
101      * config database backend
102      * 
103      * @var Tinebase_Backend_Sql
104      */
105     private static $_backend;
106     
107     /**
108      * application config class cache (name => config record)
109      * 
110      * @var array
111      */
112     protected $_cachedApplicationConfig = NULL;
113     
114     /**
115      * get properties definitions 
116      * 
117      * NOTE: as static late binding is not possible in PHP < 5.3 
118      *       this function has to be implemented in each subclass
119      *       and can not even be declared here
120      * 
121      * @return array
122      * TODO should be possible now as we no longer support PHP < 5.3
123      */
124 //    abstract public static function getProperties();
125     
126     /**
127      * get config object for application
128      * 
129      * @param string $applicationName
130      * @return Tinebase_Config_Abstract
131      */
132     public static function factory($applicationName)
133     {
134         if ($applicationName === 'Tinebase') {
135             $config = Tinebase_Core::getConfig();
136             // NOTE: this is a Zend_Config object in the Setup
137             if ($config instanceof Tinebase_Config_Abstract) {
138                 return $config;
139             }
140         }
141         
142         $configClassName = $applicationName . '_Config';
143         if (@class_exists($configClassName)) {
144             $config = call_user_func(array($configClassName, 'getInstance'));
145         } else {
146             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ 
147                 . ' Application ' . $applicationName . ' has no config class.');
148             $config = NULL;
149         }
150         
151         return $config;
152     }
153     
154     /**
155      * retrieve a value and return $default if there is no element set.
156      *
157      * @param  string $name
158      * @param  mixed  $default
159      * @return mixed
160      */
161     public function get($name, $default = NULL)
162     {
163         // NOTE: we check config file data here to prevent db lookup when db is not yet setup
164         $configFileSection = $this->_getConfigFileSection($name);
165         if ($configFileSection) {
166             return $this->_rawToConfig($configFileSection[$name], $name);
167         }
168         
169         if (Tinebase_Core::getDb() && $config = $this->_loadConfig($name)) {
170             $decodedConfigData = json_decode($config->value, TRUE);
171             // @todo JSON encode all config data via update script!
172             return $this->_rawToConfig(($decodedConfigData || is_array($decodedConfigData)) ? $decodedConfigData : $config->value, $name);
173         }
174         
175        // get default from definition if needed
176        if ($default === null) {
177            $default = $this->_getDefault($name);
178        }
179         
180         return $default;
181     }
182     
183     /**
184      * get config default
185      * - checks if application config.inc.php is available for defaults first
186      * - checks definition default second
187      * 
188      * @param string $name
189      * @return mixed
190      * 
191      * @todo merge defaults into Tinebase_Config_Struct values if available
192      */
193     protected function _getDefault($name)
194     {
195         $default = null;
196         $definition = self::getDefinition($name);
197         
198         $appDefaultConfig = $this->_getAppDefaultsConfigFileData();
199         if (isset($appDefaultConfig[$name])) {
200             $default = $appDefaultConfig[$name];
201         } else if ($definition && (isset($definition['default']) || array_key_exists('default', $definition))) {
202             $default = $definition['default'];
203         }
204         
205         if ($definition && $definition['type'] === 'object' && $definition['class'] === 'Tinebase_Config_Struct' && is_array($default)) {
206             return new Tinebase_Config_Struct($default);
207         }
208         
209         return $default;
210     }
211     
212     /**
213      * store a config value
214      *
215      * @param  string   $_name      config name
216      * @param  mixed    $_value     config value
217      * @return void
218      */
219     public function set($_name, $_value)
220     {
221         $configRecord = new Tinebase_Model_Config(array(
222             "application_id"    => Tinebase_Application::getInstance()->getApplicationByName($this->_appName)->getId(),
223             "name"              => $_name,
224             "value"             => json_encode($_value),
225         ));
226         
227         $this->_saveConfig($configRecord);
228     }
229     
230     /**
231      * delete a config from database
232      * 
233      * @param  string   $_name
234      * @return void
235      */
236     public function delete($_name)
237     {
238         $config = $this->_loadConfig($_name);
239         if ($config) {
240             $this->_getBackend()->delete($config->getId());
241             $this->clearCache(array("name" => $_name));
242         }
243     }
244     
245     /**
246      * delete all config for a application
247      *
248      * @param  string   $_applicationId
249      * @return integer  number of deleted configs
250      * 
251      * @todo remove param as this should be known?
252      */
253     public function deleteConfigByApplicationId($_applicationId)
254     {
255         $count = $this->_getBackend()->deleteByProperty($_applicationId, 'application_id');
256         $this->clearCache(array("id" => $_applicationId));
257         
258         return $count;
259     }
260     
261     /**
262      * Magic function so that $obj->value will work.
263      *
264      * @param string $name
265      * @return mixed
266      */
267     public function __get($_name)
268     {
269         return $this->get($_name);
270     }
271     
272     /**
273      * Magic function so that $obj->configName = configValue will work.
274      *
275      * @param  string   $_name      config name
276      * @param  mixed    $_value     config value
277      * @return void
278      */
279     public function __set($_name, $_value)
280     {
281         $this->set($_name, $_value);
282     }
283     
284     /**
285      * checks if a config name is set
286      * isset means that the config key is present either in config file or in db
287      * 
288      * @param  string $_name
289      * @return bool
290      */
291     public function __isset($_name)
292     {
293         // NOTE: we can't test more precise here due to cacheing
294         $value = $this->get($_name, Tinebase_Model_Config::NOTSET);
295         
296         return $value !== Tinebase_Model_Config::NOTSET;
297     }
298     
299     /**
300      * returns data from central config.inc.php file
301      * 
302      * @return array
303      */
304     protected function _getConfigFileData()
305     {
306         if (! self::$_configFileData) {
307             self::$_configFileData = include('config.inc.php');
308             
309             if (self::$_configFileData === false) {
310                 die('central configuration file config.inc.php not found in includepath: ' . get_include_path());
311             }
312             
313             if (isset(self::$_configFileData['confdfolder'])) {
314                 $tmpDir = Tinebase_Core::guessTempDir(self::$_configFileData);
315                 $cachedConfigFile = $tmpDir . DIRECTORY_SEPARATOR . 'cachedConfig.inc.php';
316
317                 if (file_exists($cachedConfigFile)) {
318                     $cachedConfigData = include($cachedConfigFile);
319                 } else {
320                     $cachedConfigData = false;
321                 }
322                 
323                 if (false === $cachedConfigData || $cachedConfigData['ttlstamp'] < time()) {
324                     $this->_createCachedConfig($tmpDir);
325                 } else {
326                     self::$_configFileData = $cachedConfigData;
327                 }
328             }
329         }
330         
331         return self::$_configFileData;
332     }
333     
334     /**
335      * composes config files from conf.d and saves array to tmp file
336      *
337      * @param string $tmpDir
338      */
339     protected function _createCachedConfig($tmpDir)
340     {
341         $confdFolder = self::$_configFileData['confdfolder'];
342
343         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
344             . ' Creating new cached config file in ' . $tmpDir);
345
346         if (! is_readable($confdFolder)) {
347             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
348                 . ' can\'t open conf.d folder "' . $confdFolder . '"');
349             return;
350         }
351
352         $dh = opendir($confdFolder);
353
354         if ($dh === false) {
355             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
356                 . ' opendir() failed on folder "' . $confdFolder . '"');
357             return;
358         }
359
360         while (false !== ($direntry = readdir($dh))) {
361             if (strpos($direntry, '.inc.php') === (strlen($direntry) - 8)) {
362                 // TODO do lint!?! php -l $confdFolder . DIRECTORY_SEPARATOR . $direntry
363                 $tmpArray = include($confdFolder . DIRECTORY_SEPARATOR . $direntry);
364                 if (false !== $tmpArray) {
365                     foreach ($tmpArray as $key => $value) {
366                         self::$_configFileData[$key] = $value;
367                     }
368                 }
369             }
370         }
371         closedir($dh);
372
373         $ttl = 60;
374         if (isset(self::$_configFileData['composeConfigTTL'])) {
375             $ttl = intval(self::$_configFileData['composeConfigTTL']);
376             if ($ttl < 1) {
377                 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
378                     . ' composeConfigTTL needs to be an integer > 0, current value: "'
379                     . print_r(self::$_configFileData['composeConfigTTL'],true) . '"');
380                 $ttl = 60;
381             }
382         }
383         self::$_configFileData['ttlstamp'] = time() + $ttl;
384         
385         $filename = $tmpDir . DIRECTORY_SEPARATOR . 'cachedConfig.inc.php';
386         $filenameTmp = $filename . uniqid();
387         $fh = fopen($filenameTmp, 'w');
388         if (false === $fh) {
389             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
390                     . ' can\'t create cached composed config file "' .$filename );
391         } else {
392             
393             fputs($fh, "<?php\n\nreturn ");
394             fputs($fh, var_export(self::$_configFileData, true));
395             fputs($fh, ';');
396             fclose($fh);
397
398             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
399                 . ' Wrote config to file ' . $filenameTmp);
400             
401             if (false === rename($filenameTmp, $filename) ) {
402                 if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__
403                     . ' can\'t rename "' . $filenameTmp . '" to "' . $filename . '"' );
404             }
405
406             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
407                 . ' Renamed to file ' . $filename);
408         }
409     }
410     
411     /**
412      * returns data from application specific config.inc.php file
413      *
414      * @return array
415      */
416     protected function _getAppDefaultsConfigFileData()
417     {
418         if (! self::$_appDefaultsConfigFileData) {
419             $cacheId = $this->_appName;
420             try {
421                 $configData = Tinebase_Cache_PerRequest::getInstance()->load(__CLASS__, __METHOD__, $cacheId);
422             } catch (Tinebase_Exception_NotFound $tenf) {
423
424                 $configFilename = dirname(dirname(dirname(__FILE__))) . DIRECTORY_SEPARATOR . $this->_appName . DIRECTORY_SEPARATOR . 'config.inc.php';
425
426                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
427                     . ' Looking for defaults config.inc.php at ' . $configFilename);
428                 if (file_exists($configFilename)) {
429                     $configData = include($configFilename);
430                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
431                         . ' Found default config.inc.php for app ' . $this->_appName);
432                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
433                         . ' ' . print_r($configData, true));
434                 } else {
435                     $configData = array();
436                 }
437                 Tinebase_Cache_PerRequest::getInstance()->save(__CLASS__, __METHOD__, $cacheId, $configData);
438             }
439             self::$_appDefaultsConfigFileData = $configData;
440         }
441         
442         return self::$_appDefaultsConfigFileData;
443     }
444     
445     /**
446      * get config file section where config identified by name is in
447      * 
448      * @param  string $_name
449      * @return array
450      */
451     protected function _getConfigFileSection($_name)
452     {
453         $configFileData = $this->_getConfigFileData();
454         
455         // appName section overwrites global section in config file
456         return (isset($configFileData[$this->_appName]) || array_key_exists($this->_appName, $configFileData)) && (isset($configFileData[$this->_appName][$_name]) || array_key_exists($_name, $configFileData[$this->_appName])) ? $configFileData[$this->_appName] :
457               ((isset($configFileData[$_name]) || array_key_exists($_name, $configFileData)) ? $configFileData : NULL);
458     }
459     
460     /**
461      * load a config record from database
462      * 
463      * @param  string                   $_name
464      * @return Tinebase_Model_Config|NULL
465      */
466     protected function _loadConfig($name)
467     {
468         if ($this->_cachedApplicationConfig === NULL) {
469             $this->_loadAllAppConfigsInCache();
470         }
471         $result = (isset($this->_cachedApplicationConfig[$name])) ? $this->_cachedApplicationConfig[$name] :  NULL;
472         
473         return $result;
474     }
475
476     /**
477     * fill class cache with all config records for this app
478     */
479     protected function _loadAllAppConfigsInCache()
480     {
481         if (empty($this->_appName)) {
482             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' appName not set');
483             $this->_cachedApplicationConfig = array();
484         }
485         
486         $cache = Tinebase_Core::getCache();
487         if (!is_object($cache)) {
488            Tinebase_Core::setupCache();
489            $cache = Tinebase_Core::getCache();
490         }
491         
492         if (Tinebase_Core::get(Tinebase_Core::SHAREDCACHE)) {
493             if ($cachedApplicationConfig = $cache->load('cachedAppConfig_' . $this->_appName)) {
494                 $this->_cachedApplicationConfig = $cachedApplicationConfig;
495                 return;
496             }
497         }
498         
499         try {
500             $applicationId = Tinebase_Model_Application::convertApplicationIdToInt($this->_appName);
501         } catch (Zend_Db_Exception $zdae) {
502             // DB might not exist or tables are not created, yet
503             Tinebase_Exception::log($zdae);
504             $this->_cachedApplicationConfig = array();
505             return;
506         }
507         
508         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Loading all configs for app ' . $this->_appName);
509         
510         $filter = new Tinebase_Model_ConfigFilter(array(
511             array('field' => 'application_id', 'operator' => 'equals', 'value' => $applicationId),
512         ));
513         $allConfigs = $this->_getBackend()->search($filter);
514         
515         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Found ' . count($allConfigs) . ' configs.');
516         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($allConfigs->toArray(), TRUE));
517         
518         foreach ($allConfigs as $config) {
519             $this->_cachedApplicationConfig[$config->name] = $config;
520         }
521         
522         if (Tinebase_Core::get(Tinebase_Core::SHAREDCACHE)) {
523             $cache->save($this->_cachedApplicationConfig, 'cachedAppConfig_' . $this->_appName);
524         }
525     }
526     
527     /**
528      * store a config record in database
529      * 
530      * @param   Tinebase_Model_Config $_config record to save
531      * @return  Tinebase_Model_Config
532      * 
533      * @todo only allow to save records for this app ($this->_appName)
534      */
535     protected function _saveConfig(Tinebase_Model_Config $_config)
536     {
537         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
538             . ' Setting config ' . $_config->name);
539         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
540             . ' ' . print_r($_config->value, true));
541         
542         $config = $this->_loadConfig($_config->name);
543         
544         if ($config) {
545             $config->value = $_config->value;
546             try {
547                 $result = $this->_getBackend()->update($config);
548             } catch (Tinebase_Exception_NotFound $tenf) {
549                 // config might be deleted but cache has not been cleaned
550                 $result = $this->_getBackend()->create($_config);
551             }
552         } else {
553             $result = $this->_getBackend()->create($_config);
554         }
555         
556         $this->clearCache();
557         
558         return $result;
559     }
560
561     /**
562      * clear the cache
563      * @param   array $appFilter
564      */
565     public function clearCache($appFilter = null)
566     {
567         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Clearing config cache');
568         $this->_cachedApplicationConfig = NULL;
569
570         if (Tinebase_Core::get(Tinebase_Core::SHAREDCACHE)) {
571             if (isset($appFilter)) {
572                 list($key, $value) = each($appFilter);
573                 $appName = $key === 'name' ? $value : Tinebase_Application::getInstance()->getApplicationById($value)->name;
574             } else {
575                 $appName = $this->_appName;
576             }
577             Tinebase_Core::getCache()->remove('cachedAppConfig_' . $appName);
578         }
579     }
580     
581     /**
582      * returns config database backend
583      * 
584      * @return Tinebase_Backend_Sql
585      */
586     protected function _getBackend()
587     {
588         if (! self::$_backend) {
589             self::$_backend = new Tinebase_Backend_Sql(array(
590                 'modelName' => 'Tinebase_Model_Config', 
591                 'tableName' => 'config',
592             ));
593         }
594         
595         return self::$_backend;
596     }
597     
598     /**
599      * converts raw data to config values of defined type
600      * 
601      * @TODO support array contents conversion
602      * @TODO support interceptors
603      * 
604      * @param   mixed     $_rawData
605      * @param   string    $_name
606      * @return  mixed
607      */
608     protected function _rawToConfig($_rawData, $_name)
609     {
610         $definition = self::getDefinition($_name);
611         
612         if (! $definition) {
613             return is_array($_rawData) ? new Tinebase_Config_Struct($_rawData) : $_rawData;
614         }
615         if ($definition['type'] === self::TYPE_OBJECT && isset($definition['class']) && @class_exists($definition['class'])) {
616             return new $definition['class']($_rawData != "null" ? $_rawData : array());
617         }
618         
619         switch ($definition['type']) {
620             case self::TYPE_INT:        return (int) $_rawData;
621             case self::TYPE_BOOL:       return (bool) (int) $_rawData;
622             case self::TYPE_STRING:     return (string) $_rawData;
623             case self::TYPE_FLOAT:      return (float) $_rawData;
624             case self::TYPE_DATETIME:   return new DateTime($_rawData);
625             case self::TYPE_KEYFIELD:   return Tinebase_Config_KeyField::create($_rawData, (isset($definition['options']) || array_key_exists('options', $definition)) ? (array) $definition['options'] : array());
626             default:                    return is_array($_rawData) ? new Tinebase_Config_Struct($_rawData) : $_rawData;
627         }
628     }
629     
630     /**
631      * get definition of given property
632      * 
633      * @param   string  $_name
634      * @return  array
635      */
636     public function getDefinition($_name)
637     {
638         // NOTE we can't call statecally here (static late binding again)
639         $properties = $this->getProperties();
640         
641         return (isset($properties[$_name]) || array_key_exists($_name, $properties)) ? $properties[$_name] : NULL;
642     }
643     
644     /**
645      * check if config system is ready
646      * 
647      * @todo check db setup
648      * @return bool
649      */
650     public static function isReady()
651     {
652         $configFile = @file_get_contents('config.inc.php', FILE_USE_INCLUDE_PATH);
653         
654         return !! $configFile;
655     }
656
657     /**
658      * returns true if a certain feature is enabled
659      * 
660      * @param string $featureName
661      * @return boolean
662      */
663     public function featureEnabled($featureName)
664     {
665         $features = $this->get(self::ENABLED_FEATURES);
666         if (isset($features->{$featureName})) {
667             return $features->{$featureName};
668         }
669         
670         return false;
671     }
672 }