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