catches client registry exceptions
[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() . "\n");
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         // TODO: this needs improvement -> it is currently not allowed to have configs with the same names in
447         //       an Application and Tinebase as this leads to strange/unpredictable results here ...
448         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] :
449               ((isset($configFileData[$_name]) || array_key_exists($_name, $configFileData)) ? $configFileData : NULL);
450     }
451     
452     /**
453      * load a config record from database
454      * 
455      * @param  string                   $_name
456      * @return Tinebase_Model_Config|NULL
457      */
458     protected function _loadConfig($name)
459     {
460         if ($this->_cachedApplicationConfig === NULL) {
461             $this->_loadAllAppConfigsInCache();
462         }
463         $result = (isset($this->_cachedApplicationConfig[$name])) ? $this->_cachedApplicationConfig[$name] :  NULL;
464         
465         return $result;
466     }
467
468     /**
469     * fill class cache with all config records for this app
470     */
471     protected function _loadAllAppConfigsInCache()
472     {
473         if (empty($this->_appName)) {
474             if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' appName not set');
475             $this->_cachedApplicationConfig = array();
476         }
477         
478         $cache = Tinebase_Core::getCache();
479         if (!is_object($cache)) {
480            Tinebase_Core::setupCache();
481            $cache = Tinebase_Core::getCache();
482         }
483         
484         if (Tinebase_Core::get(Tinebase_Core::SHAREDCACHE)) {
485             if ($cachedApplicationConfig = $cache->load('cachedAppConfig_' . $this->_appName)) {
486                 $this->_cachedApplicationConfig = $cachedApplicationConfig;
487                 return;
488             }
489         }
490         
491         try {
492             $applicationId = Tinebase_Model_Application::convertApplicationIdToInt($this->_appName);
493         } catch (Zend_Db_Exception $zdae) {
494             // DB might not exist or tables are not created, yet
495             Tinebase_Exception::log($zdae);
496             $this->_cachedApplicationConfig = array();
497             return;
498         }
499         
500         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Loading all configs for app ' . $this->_appName);
501         
502         $filter = new Tinebase_Model_ConfigFilter(array(
503             array('field' => 'application_id', 'operator' => 'equals', 'value' => $applicationId),
504         ));
505         $allConfigs = $this->_getBackend()->search($filter);
506         
507         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Found ' . count($allConfigs) . ' configs.');
508         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($allConfigs->toArray(), TRUE));
509         
510         foreach ($allConfigs as $config) {
511             $this->_cachedApplicationConfig[$config->name] = $config;
512         }
513         
514         if (Tinebase_Core::get(Tinebase_Core::SHAREDCACHE)) {
515             $cache->save($this->_cachedApplicationConfig, 'cachedAppConfig_' . $this->_appName);
516         }
517     }
518     
519     /**
520      * store a config record in database
521      * 
522      * @param   Tinebase_Model_Config $_config record to save
523      * @return  Tinebase_Model_Config
524      * 
525      * @todo only allow to save records for this app ($this->_appName)
526      */
527     protected function _saveConfig(Tinebase_Model_Config $_config)
528     {
529         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
530             . ' Setting config ' . $_config->name);
531         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
532             . ' ' . print_r($_config->value, true));
533         
534         $config = $this->_loadConfig($_config->name);
535         
536         if ($config) {
537             $config->value = $_config->value;
538             try {
539                 $result = $this->_getBackend()->update($config);
540             } catch (Tinebase_Exception_NotFound $tenf) {
541                 // config might be deleted but cache has not been cleaned
542                 $result = $this->_getBackend()->create($_config);
543             }
544         } else {
545             $result = $this->_getBackend()->create($_config);
546         }
547         
548         $this->clearCache();
549         
550         return $result;
551     }
552
553     /**
554      * clear the cache
555      * @param   array $appFilter
556      */
557     public function clearCache($appFilter = null)
558     {
559         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Clearing config cache');
560         $this->_cachedApplicationConfig = NULL;
561
562         if (Tinebase_Core::get(Tinebase_Core::SHAREDCACHE)) {
563             if (isset($appFilter)) {
564                 list($key, $value) = each($appFilter);
565                 $appName = $key === 'name' ? $value : Tinebase_Application::getInstance()->getApplicationById($value)->name;
566             } else {
567                 $appName = $this->_appName;
568             }
569             Tinebase_Core::getCache()->remove('cachedAppConfig_' . $appName);
570         }
571     }
572     
573     /**
574      * returns config database backend
575      * 
576      * @return Tinebase_Backend_Sql
577      */
578     protected function _getBackend()
579     {
580         if (! self::$_backend) {
581             self::$_backend = new Tinebase_Backend_Sql(array(
582                 'modelName' => 'Tinebase_Model_Config', 
583                 'tableName' => 'config',
584             ));
585         }
586         
587         return self::$_backend;
588     }
589     
590     /**
591      * converts raw data to config values of defined type
592      * 
593      * @TODO support array contents conversion
594      * @TODO support interceptors
595      * 
596      * @param   mixed     $_rawData
597      * @param   string    $_name
598      * @return  mixed
599      */
600     protected function _rawToConfig($_rawData, $_name)
601     {
602         $definition = self::getDefinition($_name);
603         
604         if (! $definition) {
605             return is_array($_rawData) ? new Tinebase_Config_Struct($_rawData) : $_rawData;
606         }
607         if ($definition['type'] === self::TYPE_OBJECT && isset($definition['class']) && @class_exists($definition['class'])) {
608             return new $definition['class']($_rawData != "null" ? $_rawData : array());
609         }
610         
611         switch ($definition['type']) {
612             case self::TYPE_INT:        return (int) $_rawData;
613             case self::TYPE_BOOL:       return (bool) (int) $_rawData;
614             case self::TYPE_STRING:     return (string) $_rawData;
615             case self::TYPE_FLOAT:      return (float) $_rawData;
616             case self::TYPE_DATETIME:   return new DateTime($_rawData);
617             case self::TYPE_KEYFIELD:   return Tinebase_Config_KeyField::create(
618                 $_rawData,
619                 (isset($definition['options']) || array_key_exists('options', $definition)) ? (array) $definition['options'] : array()
620             );
621             default:                    return is_array($_rawData) ? new Tinebase_Config_Struct($_rawData) : $_rawData;
622         }
623     }
624     
625     /**
626      * get definition of given property
627      * 
628      * @param   string  $_name
629      * @return  array
630      */
631     public function getDefinition($_name)
632     {
633         // NOTE we can't call statecally here (static late binding again)
634         $properties = $this->getProperties();
635         
636         return (isset($properties[$_name]) || array_key_exists($_name, $properties)) ? $properties[$_name] : NULL;
637     }
638     
639     /**
640      * check if config system is ready
641      * 
642      * @todo check db setup
643      * @return bool
644      */
645     public static function isReady()
646     {
647         $configFile = @file_get_contents('config.inc.php', FILE_USE_INCLUDE_PATH);
648         
649         return !! $configFile;
650     }
651
652     /**
653      * returns true if a certain feature is enabled
654      * 
655      * @param string $featureName
656      * @return boolean
657      */
658     public function featureEnabled($featureName)
659     {
660         $cacheId = $this->_appName;
661         try {
662             $features = Tinebase_Cache_PerRequest::getInstance()->load(__CLASS__, __METHOD__, $cacheId);
663         } catch (Tinebase_Exception_NotFound $tenf) {
664             $features = $this->get(self::ENABLED_FEATURES);
665             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
666                 . ' Features config of app ' . $this->_appName . ': '
667                 . print_r($features->toArray(), true));
668             Tinebase_Cache_PerRequest::getInstance()->save(__CLASS__, __METHOD__, $cacheId, $features);
669         }
670
671         if (isset($features->{$featureName})) {
672             return $features->{$featureName};
673         }
674
675         return false;
676     }
677 }