wCMF  3.6
 All Classes Namespaces Files Functions Variables Groups Pages
class.InifileParser.php
Go to the documentation of this file.
1 <?php
2 /**
3  * wCMF - wemove Content Management Framework
4  * Copyright (C) 2005-2014 wemove digital solutions GmbH
5  *
6  * Licensed under the terms of any of the following licenses
7  * at your choice:
8  *
9  * - GNU Lesser General Public License (LGPL)
10  * http://www.gnu.org/licenses/lgpl.html
11  * - Eclipse Public License (EPL)
12  * http://www.eclipse.org/org/documents/epl-v10.php
13  *
14  * See the license.txt file distributed with this work for
15  * additional information.
16  *
17  * $Id: class.InifileParser.php 1462 2014-02-04 23:52:27Z iherwig $
18  */
19 require_once(BASE."wcmf/lib/util/class.StringUtil.php");
20 require_once(BASE."wcmf/lib/util/class.ArrayUtil.php");
21 require_once(BASE."wcmf/lib/core/class.WCMFException.php");
22 
23 /**
24  * @class InifileParser
25  * @ingroup Util
26  * @brief InifileParser provides basic services for parsing a ini file from the file system.
27  * @note This class only supports ini files with sections.
28  *
29  * @author ingo herwig <ingo@wemove.com>
30  */
32 {
33  var $_errorMsg = '';
34  var $_filename = null;
35  var $_iniArray = array(); // an assoziate array that holds sections with keys with values
36  var $_comments = array(); // an assoziate array that holds the comments/blank lines in the file
37  // (each comment is attached to the following section/key)
38  // the key ';' holds the comments at the end of the file
39  var $_isModified = false;
40  var $_parsedFiles = array();
41  var $_useCache = true;
42 
43  /**
44  * InifileParser public readonly Interface
45  */
46 
47  /**
48  * Returns an instance of the class.
49  * @return A reference to the only instance of the Singleton object
50  */
51  function &getInstance()
52  {
53  static $instance = null;
54 
55  if (!isset($instance))
56  $instance = new InifileParser();
57 
58  return $instance;
59  }
60 
61  /**
62  * Returns the errorMsg.
63  * @return The error message.
64  */
65  function getErrorMsg()
66  {
67  return $this->_errorMsg;
68  }
69 
70  /**
71  * Check if file is modified.
72  * @return True/False whether modified.
73  */
74  function isModified()
75  {
76  return $this->_isModified;
77  }
78 
79  /**
80  * Parses an ini file and puts an array with all the key-values pairs into the object.
81  * @param filename The filename of the ini file to parse
82  * @param processValues True/False whether values should be processed after parsing (e.g. make arrays) [default: true]
83  * @note ini files referenced in section 'config' key 'include' are parsed afterwards
84  * @return True/False whether method succeeded.
85  */
86  function parseIniFile($filename, $processValues=true)
87  {
88  // do nothing, if the requested file was the last parsed file
89  if ($this->_parsedFiles[sizeof($this->_parsedFiles)-1] == $filename) {
90  return true;
91  }
92 
93  global $CONFIG_PATH;
94  if (file_exists($filename))
95  {
96  $this->_filename = $filename;
97 
98  // try to unserialize an already parsed ini file sequence
99  $tmpSequence = $this->_parsedFiles;
100  $tmpSequence[] = $filename;
101  if (!$this->unserialize($tmpSequence))
102  {
103  // merge new with old values, overwrite redefined values
104  $this->_iniArray = $this->configMerge($this->_iniArray, $this->_parse_ini_file($filename, true), true);
105 
106  // merge referenced ini files, don't override values
107  if (($includes = $this->getValue('include', 'config')) !== false)
108  {
109  $this->processValue($includes);
110  foreach($includes as $include)
111  $this->_iniArray = $this->configMerge($this->_iniArray, $this->_parse_ini_file($CONFIG_PATH.$include, true), false);
112  }
113  if ($processValues)
114  $this->processValues();
115 
116  // store the filename
117  $this->_parsedFiles[] = $filename;
118 
119  // serialize the parsed ini file sequence
120  $this->serialize();
121  }
122  return true;
123  }
124  else
125  {
126  $this->_errorMsg = "Configuration file ".$filename." not found!";
127  return false;
128  }
129  }
130 
131  /**
132  * Returns the data of the formerly parsed ini file.
133  * @return The data of the parsed ini file.
134  */
135  function getData()
136  {
137  return $this->_iniArray;
138  }
139 
140  /**
141  * Get all section names.
142  * @return An array of section names.
143  */
144  function getSections()
145  {
146  return array_keys($this->_iniArray);
147  }
148 
149  /**
150  * Get a section.
151  * @param section The section to return (case insensitive).
152  * @return An assoziative array holding the key/value pairs belonging to the section or
153  * False if the section does not exist (use getErrorMsg() for detailed information).
154  */
155  function getSection($section)
156  {
157  $matchingKeys = ArrayUtil::get_matching_values_i($section, array_keys($this->_iniArray));
158  if (sizeof($matchingKeys) > 0) {
159  $section = array_pop($matchingKeys);
160  }
161  if (!isset($this->_iniArray[$section]))
162  {
163  $this->_errorMsg = "Section '".$section."' not found!";
164  return false;
165  }
166  else {
167  return $this->_iniArray[$section];
168  }
169  }
170 
171  /**
172  * Get a value from the formerly parsed ini file.
173  * @param key The name of the entry (case insensitive).
174  * @param section The section the key belongs to (case insensitive).
175  * @return The results of the parsed ini file or
176  * False in the case of wrong parameters (use getErrorMsg() for detailed information).
177  */
178  function getValue($key, $section)
179  {
180  $sectionArray = $this->getSection($section);
181  if ($sectionArray === false) {
182  return false;
183  }
184  $matchingKeys = ArrayUtil::get_matching_values_i($key, array_keys($sectionArray));
185  if (sizeof($matchingKeys) > 0) {
186  $key = array_pop($matchingKeys);
187  }
188  if (!array_key_exists($key, $sectionArray))
189  {
190  $this->_errorMsg = "Key '".$key."' not found in section '".$section."'!";
191  return false;
192  }
193  else {
194  return $sectionArray[$key];
195  }
196  }
197 
198  /**
199  * InifileParser public modification Interface
200  */
201 
202  /**
203  * Set ini file data.
204  * @param data The ini file data.
205  */
206  function setData($data)
207  {
208  $this->_iniArray = $data;
209  $this->_isModified = true;
210  }
211 
212  /**
213  * Check if a section is hidden.
214  * @param section The name of the section.
215  * @note hidden sections are defined as an array in section 'config' key 'hiddenSections'
216  */
217  function isHidden($section)
218  {
219  if (($hiddenSections = $this->getValue('hiddenSections', 'config')) !== false)
220  {
221  $this->processValue($hiddenSections);
222  if (is_array($hiddenSections) && in_array($section, $hiddenSections))
223  return true;
224  }
225  return false;
226  }
227 
228  /**
229  * Check if a section is editable.
230  * @param section The name of the section.
231  * @note readyonly sections are defined as an array in section 'config' key 'readonlySections'
232  */
233  function isEditable($section)
234  {
235  if (($readonlySections = $this->getValue('readonlySections', 'config')) !== false)
236  {
237  $this->processValue($readonlySections);
238  if (is_array($readonlySections) && in_array($section, $readonlySections))
239  return true;
240  }
241  return true;
242  }
243 
244  /**
245  * Create a section.
246  * @param section The name of the section (will be trimmed).
247  * @return True/false whether successful.
248  */
249  function createSection($section)
250  {
251  $section = trim($section);
252  if ($this->getSection($section) !== false)
253  {
254  $this->_errorMsg = "Section '".$section."' already exists!";
255  return false;
256  }
257  if ($section == '')
258  {
259  $this->_errorMsg = "Empty section names are not allowed!";
260  return false;
261  }
262  $this->_iniArray[$section] = '';
263  $this->_isModified = true;
264  return true;
265  }
266 
267  /**
268  * Remove a section.
269  * @param section The name of the section.
270  * @return True/false whether successful.
271  */
272  function removeSection($section)
273  {
274  if (!$this->isEditable($section))
275  {
276  $this->_errorMsg = "Section ".$section." is not editable!";
277  return false;
278  }
279  if ($this->getSection($section) === false)
280  return false;
281  unset($this->_iniArray[$section]);
282  $this->_isModified = true;
283  return true;
284  }
285 
286  /**
287  * Rename a section.
288  * @param oldname The name of the section.
289  * @param newname The new name of the section (will be trimmed).
290  * @return True/false whether successful.
291  */
292  function renameSection($oldname, $newname)
293  {
294  if (!$this->isEditable($oldname))
295  {
296  $this->_errorMsg = "Section ".$oldname." is not editable!";
297  return false;
298  }
299  $newname = trim($newname);
300  if ($this->getSection($oldname) === false)
301  return false;
302  if ($this->getSection($newname) !== false)
303  {
304  $this->_errorMsg = "Section '".$newname."' already exists!";
305  return false;
306  }
307  if ($newname == '')
308  {
309  $this->_errorMsg = "Empty section names are not allowed!";
310  return false;
311  }
312  ArrayUtil::key_array_rename($this->_iniArray, $oldname, $newname);
313  $this->_isModified = true;
314  return true;
315  }
316 
317  /**
318  * Create a key/value pair in a section.
319  * @param key The name of the key (will be trimmed).
320  * @param value The value of the key.
321  * @param section The name of the section.
322  * @param createSection The name of the section.
323  * @return True/False whether successful.
324  */
325  function setValue($key, $value, $section, $createSection=true)
326  {
327  if (!$this->isEditable($section))
328  {
329  $this->_errorMsg = "Section ".$section." is not editable!";
330  return false;
331  }
332  $key = trim($key);
333  if (!$createSection && ($this->getSection($section) === false))
334  return false;
335  if ($key == '')
336  {
337  $this->_errorMsg = "Empty key names are not allowed!";
338  return false;
339  }
340  $this->_iniArray[$section][$key] = $value;
341  $this->_isModified = true;
342  return true;
343  }
344 
345  /**
346  * Remove a key from a section.
347  * @param key The name of the key.
348  * @param section The name of the section.
349  * @return True/False whether successful.
350  */
351  function removeKey($key, $section)
352  {
353  if (!$this->isEditable($section))
354  {
355  $this->_errorMsg = "Section ".$section." is not editable!";
356  return false;
357  }
358  if ($this->getValue($key, $section) === false)
359  return false;
360  unset($this->_iniArray[$section][$key]);
361  $this->_isModified = true;
362  return true;
363  }
364 
365  /**
366  * Rename a key in a section.
367  * @param oldname The name of the section.
368  * @param newname The new name of the section (will be trimmed).
369  * @param section The name of the section.
370  * @return True/false whether successful.
371  */
372  function renameKey($oldname, $newname, $section)
373  {
374  if (!$this->isEditable($section))
375  {
376  $this->_errorMsg = "Section ".$section." is not editable!";
377  return false;
378  }
379  $newname = trim($newname);
380  if ($this->getValue($oldname, $section) === false)
381  return false;
382  if ($this->getValue($newname, $section) !== false)
383  {
384  $this->_errorMsg = "Key '".$newname."' already exists in section '".$section."'!";
385  return false;
386  }
387  if ($newname == '')
388  {
389  $this->_errorMsg = "Empty key names are not allowed!";
390  return false;
391  }
392  ArrayUtil::key_array_rename($this->_iniArray[$section], $oldname, $newname);
393  $this->_isModified = true;
394  return true;
395  }
396 
397  /**
398  * Write the ini data to a file.
399  * @param filename The filename to write to, if null the original file will be used [default: null].
400  * @return True/False whether successful
401  */
402  function writeIniFile($filename=null)
403  {
404  if ($filename == null)
405  $filename = $this->_filename;
406 
407  $content = "";
408  foreach($this->_iniArray as $section => $values)
409  {
410  $sectionString = "[".$section."]";
411  $content .= $this->_comments[$sectionString];
412  $content .= $sectionString."\n";
413  if (is_array($values))
414  {
415  foreach($values as $key => $value)
416  {
417  if (is_array($value))
418  $value = "{".join(", ", $value)."}";
419  // unescape double quotes
420  $value = str_replace("\\\"", "\"", $value);
421  $content .= $this->_comments[$section][$key];
422  $content .= $key." = ".$value."\n";
423  }
424  }
425  }
426  $content .= $this->_comments[';'];
427 
428  if (!$fh = fopen($filename, 'w'))
429  {
430  $this->_errorMsg = "Can't open ini file '".$filename."'!";
431  return false;
432  }
433 
434  if (!fwrite($fh, $content))
435  {
436  $this->_errorMsg = "Can't write ini file '".$filename."'!";
437  return false;
438  }
439  fclose($fh);
440  $this->_isModified = false;
441  return true;
442  }
443 
444  /**
445  * InifileParser private Interface
446  */
447 
448  /**
449  * Load in the ini file specified in filename, and return
450  * the settings in a multidimensional array, with the section names and
451  * settings included.
452  * @param filename The filename of the ini file to parse
453  * @return An associative array containing the data / false if any error occured
454  *
455  * @author: Sebastien Cevey <seb@cine7.net>
456  * Original Code base: <info@megaman.nl>
457  * Added comment handling/Removed process sections flag: Ingo Herwig
458  */
459  function _parse_ini_file($filename)
460  {
461  if (!file_exists($filename))
462  WCMFException::throwEx("The config file ".$filename." does not exist.", __FILE__, __LINE__);
463 
464  $ini_array = array();
465  $sec_name = "";
466  $lines = file($filename);
467  $commentsPending = "";
468  foreach($lines as $line)
469  {
470  $line = trim($line);
471  // comments/blank lines
472  if($line == "" || $line[0] == ";")
473  {
474  $commentsPending .= $line."\n";
475  continue;
476  }
477 
478  if($line[0] == "[" && $line[strlen($line)-1] == "]")
479  {
480  $sec_name = substr($line, 1, strlen($line)-2);
481  $ini_array[$sec_name] = array();
482 
483  // store comments/blank lines for section
484  $this->_comments[$line] = $commentsPending;
485  $commentsPending = "";
486  }
487  else
488  {
489  $parts = explode("=", $line, 2);
490  $property = trim($parts[0]);
491  $value = trim($parts[1]);
492  $ini_array[$sec_name][$property] = $value;
493 
494  // store comments/blank lines for key
495  $this->_comments[$sec_name][$property] = $commentsPending;
496  $commentsPending = "";
497  }
498  }
499  // store comments/blank lines from the end of the file
500  $this->_comments[';'] = substr($commentsPending, 0, -1);
501 
502  return $ini_array;
503  }
504 
505  /**
506  * Process the values in the ini array.
507  * This method turns string values that hold array definitions
508  * (comma separated values enclosed by curly brackets) into array values.
509  * @attention Internal use only.
510  */
511  function processValues()
512  {
513  array_walk_recursive($this->_iniArray, array($this, 'processValue'));
514  }
515 
516  /**
517  * Process the values in the ini array.
518  * This method turns string values that hold array definitions
519  * (comma separated values enclosed by curly brackets) into array values.
520  * @param value A reference to the value
521  * @attention Internal use only.
522  */
523  function processValue(&$value)
524  {
525  if (!is_array($value))
526  {
527  // decode encoded (%##) values
528  if (preg_match ("/%/", $value))
529  $value = urldecode($value);
530  // make arrays
531  if(preg_match("/^{.*}$/", $value))
532  {
533  $arrayValues = StringUtil::quotesplit(substr($value, 1, -1));
534  $value = array();
535  foreach ($arrayValues as $arrayValue)
536  array_push($value, trim($arrayValue));
537  }
538  }
539  }
540 
541  /**
542  * Merge two arrays, preserving entries in first one unless they are
543  * overridden by ones in the second.
544  * @param array1 First array.
545  * @param array2 Second array.
546  * @param override True/False whether values defined in array1 should be overriden by values defined in array2.
547  * @return The merged array.
548  */
549  function configMerge($array1, $array2, $override)
550  {
551  $result = $array1;
552  foreach(array_keys($array2) as $key)
553  {
554  if (!array_key_exists($key, $result))
555  $result[$key] = $array2[$key];
556  else
557  foreach(array_keys($array2[$key]) as $subkey)
558  {
559  if ((array_key_exists($subkey, $result[$key]) && $override) || !isset($result[$key][$subkey]))
560  $result[$key][$subkey] = $array2[$key][$subkey];
561  }
562  }
563  return $result;
564  }
565 
566  /**
567  * Store the instance in the filesystem. If the instance is modified, this call is ignored.
568  */
569  function serialize()
570  {
571  if ($this->_useCache && !$this->isModified())
572  {
573  $cacheFile = $this->getSerializeFilename($this->_parsedFiles);
574  if($fh = @fopen($cacheFile, "w"))
575  {
576  if(@fwrite($fh, serialize(get_object_vars($this))))
577  {
578  @fclose($f);
579  }
580  }
581  }
582  }
583 
584  /**
585  * Retrieve parsed ini data from the filesystem and update the current instance.
586  * If the current instance is modified or the last file given in parsedFiles
587  * is newer than the seriralized data, this call is ignored.
588  * @param parsedFiles An array of ini filenames that must be contained in the data.
589  * @param True/False wether the data could be retrieved or not
590  */
591  function unserialize($parsedFiles)
592  {
593  if ($this->_useCache && !$this->isModified())
594  {
595  $cacheFile = $this->getSerializeFilename($parsedFiles);
596  if (file_exists($cacheFile))
597  {
598  if (!$this->checkFileDate($parsedFiles, $cacheFile))
599  {
600  $vars = unserialize(file_get_contents($cacheFile));
601 
602  // check if included ini files were updated since last cache time
603  if (isset($vars['_iniArray']['config']))
604  {
605  global $CONFIG_PATH;
606  $includes = $vars['_iniArray']['config']['include'];
607  if (is_array($includes))
608  {
609  $includedFiles = array();
610  foreach($includes as $include) {
611  $includedFiles[] = $CONFIG_PATH.$include;
612  }
613  if ($this->checkFileDate($includedFiles, $cacheFile)) {
614  return false;
615  }
616  }
617  }
618 
619  // everything is up-to-date
620  foreach($vars as $key=>$val)
621  {
622  eval("$"."this->$key = $"."vars['"."$key'];");
623  }
624  return true;
625  }
626  }
627  }
628  return false;
629  }
630 
631  /**
632  * Get the filename for the serialized data that correspond to the the given ini file sequence.
633  * @param parsedFiles An array of parsed filenames
634  */
635  function getSerializeFilename($parsedFiles)
636  {
637  global $CONFIG_PATH;
638  $path = session_save_path();
639  $filename = $path.'/'.urlencode(realpath($CONFIG_PATH)."/".join('_', $parsedFiles));
640  return $filename;
641  }
642 
643  /**
644  * Check if one file in fileList is newer than the referenceFile.
645  * @param fileList An array of files
646  * @param referenceFile The file to check against
647  * @return True, if one of the files is newer, false else
648  */
649  function checkFileDate($fileList, $referenceFile)
650  {
651  foreach ($fileList as $file) {
652  if (filemtime($file) > filemtime($referenceFile)) {
653  return true;
654  }
655  }
656  return false;
657  }
658 }
659 ?>
parseIniFile($filename, $processValues=true)
get_matching_values_i($str, $array)
_parse_ini_file($filename)
throwEx($message, $file='', $line='')
key_array_rename(&$input, $oldname, $newname)
removeKey($key, $section)
checkFileDate($fileList, $referenceFile)
InifileParser provides basic services for parsing a ini file from the file system.
renameKey($oldname, $newname, $section)
renameSection($oldname, $newname)
getValue($key, $section)
writeIniFile($filename=null)
configMerge($array1, $array2, $override)
unserialize($parsedFiles)
setValue($key, $value, $section, $createSection=true)
getSerializeFilename($parsedFiles)