1<?php
2/*
3 *  Configuration Class and generic setting classes
4 *
5 *  @author  Chris Smith <chris@jalakai.co.uk>
6 */
7
8if (!class_exists('configuration0')) {
9
10  class configuration0 {
11
12    var $_name = 'conf';           // name of the config variable found in the files (overridden by $config['varname'])
13    var $_format = 'php';          // format of the config file, supported formats - php (overridden by $config['format'])
14    var $_heading = '';            // heading string written at top of config file - don't include comment indicators
15    var $_loaded = false;          // set to true after configuration files are loaded
16    var $_metadata = array();      // holds metadata describing the settings
17    var $setting = array();        // array of setting objects
18    var $locked = false;           // configuration is considered locked if it can't be updated
19
20    // filenames, these will be eval()'d prior to use so maintain any constants in output
21    var $_default_file  = '';
22    var $_local_file = '';
23    var $_protected_file = '';
24
25    /**
26     *  constructor
27     */
28    function configuration0($datafile) {
29        global $conf;
30
31        if (!@file_exists($datafile)) {
32          msg('No configuration metadata found at - '.htmlspecialchars($datafile),-1);
33          return;
34        }
35        include($datafile);
36
37        if (isset($config['varname'])) $this->_name = $config['varname'];
38        if (isset($config['format'])) $this->_format = $config['format'];
39        if (isset($config['heading'])) $this->_heading = $config['heading'];
40
41        if (isset($file['default'])) $this->_default_file = $file['default'];
42        if (isset($file['local'])) $this->_local_file = $file['local'];
43        if (isset($file['protected'])) $this->_protected_file = $file['protected'];
44
45        $this->locked = $this->_is_locked();
46
47        $this->_metadata = array_merge($meta, $this->get_plugintpl_metadata($conf['template']));
48
49        $this->retrieve_settings();
50    }
51
52    function retrieve_settings() {
53        global $conf;
54
55        if (!$this->_loaded) {
56          $default = array_merge($this->_read_config($this->_default_file), $this->get_plugintpl_default($conf['template']));
57          $local = $this->_read_config($this->_local_file);
58          $protected = $this->_read_config($this->_protected_file);
59
60          $keys = array_merge(array_keys($this->_metadata),array_keys($default), array_keys($local), array_keys($protected));
61          $keys = array_unique($keys);
62
63          foreach ($keys as $key) {
64            if (isset($this->_metadata[$key])) {
65              $class = $this->_metadata[$key][0];
66              $class = ($class && class_exists('setting_'.$class)) ? 'setting_'.$class : 'setting';
67
68              $param = $this->_metadata[$key];
69              array_shift($param);
70            } else {
71              $class = 'setting';
72              $param = NULL;
73            }
74
75            $this->setting[$key] = new $class($key,$param);
76            $this->setting[$key]->initialize($default[$key],$local[$key],$protected[$key]);
77          }
78
79          $this->_loaded = true;
80        }
81    }
82
83    function save_settings($id, $header='', $backup=true) {
84
85      if ($this->locked) return false;
86
87      $file = eval('return '.$this->_local_file.';');
88
89      // backup current file (remove any existing backup)
90      if (@file_exists($file) && $backup) {
91        if (@file_exists($file.'.bak')) @unlink($file.'.bak');
92        if (!@rename($file, $file.'.bak')) return false;
93      }
94
95      if (!$fh = @fopen($file, 'wb')) {
96        @rename($file.'.bak', $file);     // problem opening, restore the backup
97        return false;
98      }
99
100      if (empty($header)) $header = $this->_heading;
101
102      $out = $this->_out_header($id,$header);
103
104      foreach ($this->setting as $setting) {
105        $out .= $setting->out($this->_name, $this->_format);
106      }
107
108      $out .= $this->_out_footer();
109
110      @fwrite($fh, $out);
111      fclose($fh);
112      return true;
113    }
114
115    /**
116     * return an array of config settings
117     */
118    function _read_config($file) {
119
120      if (!$file) return array();
121
122      $config = array();
123      $file = eval('return '.$file.';');
124
125      if ($this->_format == 'php') {
126
127        $contents = @php_strip_whitespace($file);
128        $pattern = '/\$'.$this->_name.'\[[\'"]([^=]+)[\'"]\] ?= ?(.*?);/';
129        $matches=array();
130        preg_match_all($pattern,$contents,$matches,PREG_SET_ORDER);
131
132        for ($i=0; $i<count($matches); $i++) {
133
134          // correct issues with the incoming data
135          // FIXME ... for now merge multi-dimensional array indices using ____
136          $key = preg_replace('/.\]\[./',CM_KEYMARKER,$matches[$i][1]);
137
138          // remove quotes from quoted strings & unescape escaped data
139          $value = preg_replace('/^(\'|")(.*)(?<!\\\\)\1$/','$2',$matches[$i][2]);
140          $value = strtr($value, array('\\\\'=>'\\','\\\''=>'\'','\\"'=>'"'));
141
142          $config[$key] = $value;
143        }
144      }
145
146      return $config;
147    }
148
149    function _out_header($id, $header) {
150      $out = '';
151      if ($this->_format == 'php') {
152          $out .= '<'.'?php'."\n".
153                "/*\n".
154                " * ".$header." \n".
155                " * Auto-generated by ".$id." plugin \n".
156                " * Run for user: ".$_SERVER['REMOTE_USER']."\n".
157                " * Date: ".date('r')."\n".
158                " */\n\n";
159      }
160
161      return $out;
162    }
163
164    function _out_footer() {
165      $out = '';
166      if ($this->_format == 'php') {
167          if ($this->_protected_file) {
168            $out .= "\n@include(".$this->_protected_file.");\n";
169          }
170          $out .= "\n// end auto-generated content\n";
171      }
172
173      return $out;
174    }
175
176    // configuration is considered locked if there is no local settings filename
177    // or the directory its in is not writable or the file exists and is not writable
178    function _is_locked() {
179      if (!$this->_local_file) return true;
180
181      $local = eval('return '.$this->_local_file.';');
182
183      if (!is_writable(dirname($local))) return true;
184      if (file_exists($local) && !is_writable($local)) return true;
185
186      return false;
187    }
188
189    /**
190     *  not used ... conf's contents are an array!
191     *  reduce any multidimensional settings to one dimension using CM_KEYMARKER
192     */
193    function _flatten($conf,$prefix='') {
194
195        $out = array();
196
197        foreach($conf as $key => $value) {
198          if (!is_array($value)) {
199            $out[$prefix.$key] = $value;
200            continue;
201          }
202
203          $tmp = $this->_flatten($value,$prefix.$key.CM_KEYMARKER);
204          $out = array_merge($out,$tmp);
205        }
206
207        return $out;
208    }
209
210    /**
211     * load metadata for plugin and template settings
212     */
213    function get_plugintpl_metadata($tpl){
214      $file     = '/conf/metadata.php';
215      $metadata = array();
216
217      if ($dh = opendir(DOKU_PLUGIN)) {
218        while (false !== ($plugin = readdir($dh))) {
219          if ($plugin == '.' || $plugin == '..' || $plugin == 'tmp' || $plugin == 'config') continue;
220          if (is_file(DOKU_PLUGIN.$plugin)) continue;
221
222          if (@file_exists(DOKU_PLUGIN.$plugin.$file)){
223            $meta = array();
224            @include(DOKU_PLUGIN.$plugin.$file);
225            foreach ($meta as $key => $value){
226              $metadata['plugin'.CM_KEYMARKER.$plugin.CM_KEYMARKER.$key] = $value;
227            }
228          }
229        }
230        closedir($dh);
231      }
232
233      // the same for the active template
234      if (@file_exists(DOKU_TPLINC.$file)){
235        $meta = array();
236        @include(DOKU_TPLINC.$file);
237        foreach ($meta as $key => $value){
238          $metadata['tpl'.CM_KEYMARKER.$tpl.CM_KEYMARKER.$key] = $value;
239        }
240      }
241
242      return $metadata;
243    }
244
245    /**
246     * load default settings for plugins and templates
247     */
248    function get_plugintpl_default($tpl){
249      $file    = '/conf/default.php';
250      $default = array();
251
252      if ($dh = opendir(DOKU_PLUGIN)) {
253        while (false !== ($plugin = readdir($dh))) {
254          if (@file_exists(DOKU_PLUGIN.$plugin.$file)){
255            $conf = array();
256            @include(DOKU_PLUGIN.$plugin.$file);
257            foreach ($conf as $key => $value){
258              $default['plugin'.CM_KEYMARKER.$plugin.CM_KEYMARKER.$key] = $value;
259            }
260          }
261        }
262        closedir($dh);
263      }
264
265      // the same for the active template
266      if (@file_exists(DOKU_TPLINC.$file)){
267        $conf = array();
268        @include(DOKU_TPLINC.$file);
269        foreach ($conf as $key => $value){
270          $default['tpl'.CM_KEYMARKER.$tpl.CM_KEYMARKER.$key] = $value;
271        }
272      }
273
274      return $default;
275    }
276
277  }
278}
279
280if (!class_exists('setting0')) {
281  class setting0 {
282
283    var $_key = '';
284    var $_default = NULL;
285    var $_local = NULL;
286    var $_protected = NULL;
287
288    var $_pattern = '';
289    var $_error = false;            // only used by those classes which error check
290    var $_input = NULL;             // only used by those classes which error check
291
292    function setting0($key, $params=NULL) {
293        $this->_key = $key;
294
295        if (is_array($params)) {
296          foreach($params as $property => $value) {
297            $this->$property = $value;
298          }
299        }
300    }
301
302    /**
303     *  recieves current values for the setting $key
304     */
305    function initialize($default, $local, $protected) {
306        if (isset($default)) $this->_default = $default;
307        if (isset($local)) $this->_local = $local;
308        if (isset($protected)) $this->_protected = $protected;
309    }
310
311    /**
312     *  update setting with user provided value $input
313     *  if value fails error check, save it
314     *
315     *  @return true if changed, false otherwise (incl. on error)
316     */
317    function update($input) {
318        if (is_null($input)) return false;
319        if ($this->is_protected()) return false;
320
321        $value = is_null($this->_local) ? $this->_default : $this->_local;
322        if ($value == $input) return false;
323
324        if ($this->_pattern && !preg_match($this->_pattern,$input)) {
325          $this->_error = true;
326          $this->_input = $input;
327          return false;
328        }
329
330        $this->_local = $input;
331        return true;
332    }
333
334    /**
335     *  @return   array(string $label_html, string $input_html)
336     */
337    function html(&$plugin, $echo=false) {
338        $value = '';
339        $disable = '';
340
341        if ($this->is_protected()) {
342          $value = $this->_protected;
343          $disable = 'disabled="disabled"';
344        } else {
345          if ($echo && $this->_error) {
346            $value = $this->_input;
347          } else {
348            $value = is_null($this->_local) ? $this->_default : $this->_local;
349          }
350        }
351
352        $key = htmlspecialchars($this->_key);
353        $value = htmlspecialchars($value);
354
355        $label = '<label for="config__'.$key.'">'.$this->prompt($plugin).'</label>';
356        $input = '<textarea rows="3" cols="40" id="config__'.$key.'" name="config['.$key.']" class="edit" '.$disable.'>'.$value.'</textarea>';
357        return array($label,$input);
358    }
359
360    /**
361     *  generate string to save setting value to file according to $fmt
362     */
363    function out($var, $fmt='php') {
364
365      if ($this->is_protected()) return '';
366      if (is_null($this->_local) || ($this->_default == $this->_local)) return '';
367
368      $out = '';
369
370      if ($fmt=='php') {
371        // translation string needs to be improved FIXME
372        $tr = array("\n"=>'\n', "\r"=>'\r', "\t"=>'\t', "\\" => '\\\\', "'" => '\\\'');
373        $tr = array("\\" => '\\\\', "'" => '\\\'');
374
375        $out =  '$'.$var."['".$this->_out_key()."'] = '".strtr($this->_local, $tr)."';\n";
376      }
377
378      return $out;
379    }
380
381    function prompt(&$plugin) {
382      $prompt = $plugin->getLang($this->_key);
383      if (!$prompt) $prompt = htmlspecialchars(str_replace(array('____','_'),' ',$this->_key));
384      return $prompt;
385    }
386
387    function is_protected() { return !is_null($this->_protected); }
388    function is_default() { return !$this->is_protected() && is_null($this->_local); }
389    function error() { return $this->_error; }
390
391    function _out_key() { return str_replace(CM_KEYMARKER,"']['",$this->_key); }
392  }
393}
394
395
396/**
397 *  Provide php_strip_whitespace (php5 function) functionality
398 *
399 *  @author   Chris Smith <chris@jalakai.co.uk>
400 */
401if (!function_exists('php_strip_whitespace'))  {
402
403  if (function_exists('token_get_all')) {
404
405    if (!defined('T_ML_COMMENT')) {
406      define('T_ML_COMMENT', T_COMMENT);
407    } else {
408      define('T_DOC_COMMENT', T_ML_COMMENT);
409    }
410
411    /**
412     * modified from original
413     * source Google Groups, php.general, by David Otton
414     */
415    function php_strip_whitespace($file) {
416        if (!@is_readable($file)) return '';
417
418        $in = join('',@file($file));
419        $out = '';
420
421        $tokens = token_get_all($in);
422
423        foreach ($tokens as $token) {
424          if (is_string ($token)) {
425            $out .= $token;
426          } else {
427            list ($id, $text) = $token;
428            switch ($id) {
429              case T_COMMENT : // fall thru
430              case T_ML_COMMENT : // fall thru
431              case T_DOC_COMMENT : // fall thru
432              case T_WHITESPACE :
433                break;
434              default : $out .= $text; break;
435            }
436          }
437        }
438        return ($out);
439    }
440
441  } else {
442
443    function is_whitespace($c) { return (strpos("\t\n\r ",$c) !== false); }
444    function is_quote($c) { return (strpos("\"'",$c) !== false); }
445    function is_escaped($s,$i) {
446        $idx = $i-1;
447        while(($idx>=0) && ($s{$idx} == '\\')) $idx--;
448        return (($i - $idx + 1) % 2);
449    }
450
451    function is_commentopen($str, $i) {
452        if ($str{$i} == '#') return "\n";
453        if ($str{$i} == '/') {
454          if ($str{$i+1} == '/') return "\n";
455          if ($str{$i+1} == '*') return "*/";
456        }
457
458        return false;
459    }
460
461    function php_strip_whitespace($file) {
462
463        if (!@is_readable($file)) return '';
464
465        $contents = join('',@file($file));
466        $out = '';
467
468        $state = 0;
469        for ($i=0; $i<strlen($contents); $i++) {
470          if (!$state && is_whitespace($contents{$i})) continue;
471
472          if (!$state && ($c_close = is_commentopen($contents, $i))) {
473            $c_open_len = ($contents{$i} == '/') ? 2 : 1;
474            $i = strpos($contents, $c_close, $i+$c_open_len)+strlen($c_close)-1;
475            continue;
476          }
477
478          $out .= $contents{$i};
479          if (is_quote($contents{$i})) {
480              if (($state == $contents{$i}) && !is_escaped($contents, $i)) { $state = 0; continue; }
481            if (!$state) {$state = $contents{$i}; continue; }
482          }
483        }
484
485        return $out;
486    }
487  }
488}
489