1 <?php
  2 
  3 /**
  4  * Class for handling configurations.
  5  * The configuration is persisted between requests.
  6  *
  7  * @author staabm
  8  *
  9  * @package redaxo\core
 10  */
 11 class rex_config
 12 {
 13     /**
 14      * Flag to indicate if the config was initialized.
 15      *
 16      * @var bool
 17      */
 18     private static $initialized = false;
 19 
 20     /*
 21      * path to the cache file
 22      *
 23      * @var string
 24      */
 25     private static $cacheFile;
 26 
 27     /**
 28      * Flag which indicates if database needs an update, because settings have changed.
 29      *
 30      * @var bool
 31      */
 32     private static $changed = false;
 33 
 34     /**
 35      * data read from database.
 36      *
 37      * @var array
 38      */
 39     private static $data = [];
 40 
 41     /**
 42      * data which is modified during this request.
 43      *
 44      * @var array
 45      */
 46     private static $changedData = [];
 47 
 48     /**
 49      * data which was deleted during this request.
 50      *
 51      * @var array
 52      */
 53     private static $deletedData = [];
 54 
 55     /**
 56      * Method which saves an arbitary value associated to the given namespace and key.
 57      * If the second parameter is an associative array, all key/value pairs will be saved.
 58      *
 59      * The set-method returns TRUE when an existing value was overridden, otherwise FALSE is returned.
 60      *
 61      * @param string       $namespace The namespace e.g. an addon name
 62      * @param string|array $key       The associated key or an associative array of key/value pairs
 63      * @param mixed        $value     The value to save
 64      *
 65      * @throws InvalidArgumentException
 66      *
 67      * @return bool TRUE when an existing value was overridden, otherwise FALSE
 68      */
 69     public static function set($namespace, $key, $value = null)
 70     {
 71         self::init();
 72 
 73         if (!is_string($namespace)) {
 74             throw new InvalidArgumentException('rex_config: expecting $namespace to be a string, ' . gettype($namespace) . ' given!');
 75         }
 76 
 77         if (is_array($key)) {
 78             $existed = false;
 79             foreach ($key as $k => $v) {
 80                 $existed = self::set($namespace, $k, $v) || $existed;
 81             }
 82             return $existed;
 83         }
 84 
 85         if (!is_string($key)) {
 86             throw new InvalidArgumentException('rex_config: expecting $key to be a string, ' . gettype($key) . ' given!');
 87         }
 88 
 89         if (!isset(self::$data[$namespace])) {
 90             self::$data[$namespace] = [];
 91         }
 92 
 93         $existed = isset(self::$data[$namespace][$key]);
 94         if (!$existed || $existed && self::$data[$namespace][$key] !== $value) {
 95             // keep track of changed data
 96             self::$changedData[$namespace][$key] = $value;
 97 
 98             // since it was re-added, do not longer mark as deleted
 99             unset(self::$deletedData[$namespace][$key]);
100 
101             // re-set the data in the container
102             self::$data[$namespace][$key] = $value;
103             self::$changed = true;
104         }
105 
106         return $existed;
107     }
108 
109     /**
110      * Method which returns an associated value for the given namespace and key.
111      * If $key is null, an array of all key/value pairs for the given namespace will be returned.
112      *
113      * If no value can be found for the given key/namespace combination $default is returned.
114      *
115      * @param string $namespace The namespace e.g. an addon name
116      * @param string $key       The associated key
117      * @param mixed  $default   Default return value if no associated-value can be found
118      *
119      * @throws InvalidArgumentException
120      *
121      * @return mixed the value for $key or $default if $key cannot be found in the given $namespace
122      */
123     public static function get($namespace, $key = null, $default = null)
124     {
125         self::init();
126 
127         if (!is_string($namespace)) {
128             throw new InvalidArgumentException('rex_config: expecting $namespace to be a string, ' . gettype($namespace) . ' given!');
129         }
130 
131         if ($key === null) {
132             return isset(self::$data[$namespace]) ? self::$data[$namespace] : [];
133         }
134 
135         if (!is_string($key)) {
136             throw new InvalidArgumentException('rex_config: expecting $key to be a string, ' . gettype($key) . ' given!');
137         }
138 
139         if (isset(self::$data[$namespace][$key])) {
140             return self::$data[$namespace][$key];
141         }
142         return $default;
143     }
144 
145     /**
146      * Returns if the given key is set.
147      *
148      * @param string $namespace The namespace e.g. an addon name
149      * @param string $key       The associated key
150      *
151      * @throws InvalidArgumentException
152      *
153      * @return bool TRUE if the key is set, otherwise FALSE
154      */
155     public static function has($namespace, $key = null)
156     {
157         self::init();
158 
159         if (!is_string($namespace)) {
160             throw new InvalidArgumentException('rex_config: expecting $namespace to be a string, ' . gettype($namespace) . ' given!');
161         }
162 
163         if ($key === null) {
164             return isset(self::$data[$namespace]);
165         }
166 
167         if (!is_string($key)) {
168             throw new InvalidArgumentException('rex_config: expecting $key to be a string, ' . gettype($key) . ' given!');
169         }
170 
171         return isset(self::$data[$namespace][$key]);
172     }
173 
174     /**
175      * Removes the setting associated with the given namespace and key.
176      *
177      * @param string $namespace The namespace e.g. an addon name
178      * @param string $key       The associated key
179      *
180      * @throws InvalidArgumentException
181      *
182      * @return bool TRUE if the value was found and removed, otherwise FALSE
183      */
184     public static function remove($namespace, $key)
185     {
186         self::init();
187 
188         if (!is_string($namespace)) {
189             throw new InvalidArgumentException('rex_config: expecting $namespace to be a string, ' . gettype($namespace) . ' given!');
190         }
191         if (!is_string($key)) {
192             throw new InvalidArgumentException('rex_config: expecting $key to be a string, ' . gettype($key) . ' given!');
193         }
194 
195         if (isset(self::$data[$namespace][$key])) {
196             // keep track of deleted data
197             self::$deletedData[$namespace][$key] = true;
198 
199             // since it will be deleted, do not longer mark as changed
200             unset(self::$changedData[$namespace][$key]);
201 
202             // delete the data from the container
203             unset(self::$data[$namespace][$key]);
204             if (empty(self::$data[$namespace])) {
205                 unset(self::$data[$namespace]);
206             }
207             self::$changed = true;
208             return true;
209         }
210         return false;
211     }
212 
213     /**
214      * Removes all settings associated with the given namespace.
215      *
216      * @param string $namespace The namespace e.g. an addon name
217      *
218      * @throws InvalidArgumentException
219      *
220      * @return bool TRUE if the namespace was found and removed, otherwise FALSE
221      */
222     public static function removeNamespace($namespace)
223     {
224         self::init();
225 
226         if (!is_string($namespace)) {
227             throw new InvalidArgumentException('rex_config: expecting $namespace to be a string, ' . gettype($namespace) . ' given!');
228         }
229 
230         if (isset(self::$data[$namespace])) {
231             foreach (self::$data[$namespace] as $key => $value) {
232                 self::remove($namespace, $key);
233             }
234 
235             unset(self::$data[$namespace]);
236             self::$changed = true;
237             return true;
238         }
239         return false;
240     }
241 
242     /**
243      * Refreshes rex_config by reloading config from db.
244      */
245     public static function refresh()
246     {
247         if (!self::$initialized) {
248             self::init();
249 
250             return;
251         }
252 
253         self::loadFromDb();
254 
255         self::generateCache();
256 
257         self::$changed = false;
258         self::$changedData = [];
259         self::$deletedData = [];
260     }
261 
262     /**
263      * initilizes the rex_config class.
264      */
265     protected static function init()
266     {
267         if (self::$initialized) {
268             return;
269         }
270 
271         self::$cacheFile = rex_path::coreCache('config.cache');
272 
273         // take care, so we are able to write a cache file on shutdown
274         // (check here, since exceptions in shutdown functions are not visible to the user)
275         $dir = dirname(self::$cacheFile);
276         rex_dir::create($dir);
277         if (!is_writable($dir)) {
278             throw new rex_exception('rex-config: cache dir "' . dirname(self::$cacheFile) . '" is not writable!');
279         }
280 
281         // save cache on shutdown
282         register_shutdown_function([self::class, 'save']);
283 
284         self::load();
285         self::$initialized = true;
286     }
287 
288     /**
289      * load the config-data.
290      */
291     protected static function load()
292     {
293         // check if we can load the config from the filesystem
294         if (!self::loadFromFile()) {
295             // if not possible, fallback to load config from the db
296             self::loadFromDb();
297             // afterwards persist loaded data into file-cache
298             self::generateCache();
299         }
300     }
301 
302     /**
303      * load the config-data from a file-cache.
304      *
305      * @return bool Returns TRUE, if the data was successfully loaded from the file-cache, otherwise FALSE
306      */
307     private static function loadFromFile()
308     {
309         // delete cache-file, will be regenerated on next request
310         if (file_exists(self::$cacheFile)) {
311             self::$data = rex_file::getCache(self::$cacheFile);
312             return true;
313         }
314         return false;
315     }
316 
317     /**
318      * load the config-data from database.
319      */
320     private static function loadFromDb()
321     {
322         $sql = rex_sql::factory();
323         $sql->setQuery('SELECT * FROM ' . rex::getTablePrefix() . 'config');
324 
325         self::$data = [];
326         foreach ($sql as $cfg) {
327             self::$data[$cfg->getValue('namespace')][$cfg->getValue('key')] = json_decode($cfg->getValue('value'), true);
328         }
329     }
330 
331     /**
332      * save config to file-cache.
333      */
334     private static function generateCache()
335     {
336         if (rex_file::putCache(self::$cacheFile, self::$data) <= 0) {
337             throw new rex_exception('rex-config: unable to write cache file ' . self::$cacheFile);
338         }
339     }
340 
341     /**
342      * persists the config-data and truncates the file-cache.
343      */
344     public static function save()
345     {
346         // save cache only if changes happened
347         if (!self::$changed) {
348             return;
349         }
350 
351         // after all no data needs to be deleted or update, so skip save
352         if (empty(self::$deletedData) && empty(self::$changedData)) {
353             return;
354         }
355 
356         // delete cache-file; will be regenerated on next request
357         rex_file::delete(self::$cacheFile);
358 
359         // save all data to the db
360         self::saveToDb();
361         self::$changed = false;
362         self::$changedData = [];
363         self::$deletedData = [];
364     }
365 
366     /**
367      * save the config-data into the db.
368      */
369     private static function saveToDb()
370     {
371         $sql = rex_sql::factory();
372         // $sql->setDebug();
373 
374         // remove all deleted data
375         if (self::$deletedData) {
376             $sql->setTable(rex::getTable('config'));
377 
378             $where = [];
379             $params = [];
380             foreach (self::$deletedData as $namespace => $nsData) {
381                 $params = array_merge($params, [$namespace], array_keys($nsData));
382                 $where[] = 'namespace = ? AND `key` IN ('.implode(', ', array_fill(0, count($nsData), '?')).')';
383             }
384 
385             $sql->setWhere(implode("\n    OR ", $where), $params);
386             $sql->delete();
387         }
388 
389         // update all changed data
390         if (self::$changedData) {
391             $sql->setTable(rex::getTable('config'));
392 
393             foreach (self::$changedData as $namespace => $nsData) {
394                 foreach ($nsData as $key => $value) {
395                     $sql->addRecord(function (rex_sql $record) use ($namespace, $key, $value) {
396                         $record->setValue('namespace', $namespace);
397                         $record->setValue('key', $key);
398                         $record->setValue('value', json_encode($value));
399                     });
400                 }
401             }
402 
403             $sql->insertOrUpdate();
404         }
405     }
406 }
407