MDL-67850 lib: add new plist library
[moodle.git] / lib / plist / classes / CFPropertyList / CFPropertyList.php
1 <?php
2 /**
3  * CFPropertyList
4  * {@link http://developer.apple.com/documentation/Darwin/Reference/ManPages/man5/plist.5.html Property Lists}
5  * @author Rodney Rehm <rodney.rehm@medialize.de>
6  * @author Christian Kruse <cjk@wwwtech.de>
7  * @package plist
8  * @version $Id$
9  * @example example-read-01.php Read an XML PropertyList
10  * @example example-read-02.php Read a Binary PropertyList
11  * @example example-read-03.php Read a PropertyList without knowing the type
12  * @example example-create-01.php Using the CFPropertyList API
13  * @example example-create-02.php Using {@link CFTypeDetector}
14  * @example example-create-03.php Using {@link CFTypeDetector} with {@link CFDate} and {@link CFData}
15  * @example example-modify-01.php Read, modify and save a PropertyList
16  */
18 namespace CFPropertyList;
19 use \Iterator, \DOMDocument, \DOMException, DOMImplementation, DOMNode;
21 /**
22  * Require IOException, PListException, CFType and CFBinaryPropertyList
23  */
24 require_once(__DIR__.'/IOException.php');
25 require_once(__DIR__.'/PListException.php');
26 require_once(__DIR__.'/CFType.php');
27 require_once(__DIR__.'/CFBinaryPropertyList.php');
28 require_once(__DIR__.'/CFTypeDetector.php');
30 /**
31  * Property List
32  * Interface for handling reading, editing and saving Property Lists as defined by Apple.
33  * @author Rodney Rehm <rodney.rehm@medialize.de>
34  * @author Christian Kruse <cjk@wwwtech.de>
35  * @package plist
36  * @example example-read-01.php Read an XML PropertyList
37  * @example example-read-02.php Read a Binary PropertyList
38  * @example example-read-03.php Read a PropertyList without knowing the type
39  * @example example-create-01.php Using the CFPropertyList API
40  * @example example-create-02.php Using {@link CFTypeDetector}
41  * @example example-create-03.php Using {@link CFTypeDetector} with {@link CFDate} and {@link CFData}
42  * @example example-create-04.php Using and extended {@link CFTypeDetector}
43  */
44 class CFPropertyList extends CFBinaryPropertyList implements Iterator {
45   /**
46    * Format constant for binary format
47    * @var integer
48    */
49   const FORMAT_BINARY = 1;
51   /**
52    * Format constant for xml format
53    * @var integer
54    */
55   const FORMAT_XML = 2;
57   /**
58    * Format constant for automatic format recognizing
59    * @var integer
60    */
61   const FORMAT_AUTO = 0;
63   /**
64    * Path of PropertyList
65    * @var string
66    */
67   protected $file = null;
68   
69   /**
70    * Detected format of PropertyList
71    * @var integer
72    */
73   protected $detectedFormat = null;
75   /**
76    * Path of PropertyList
77    * @var integer
78    */
79   protected $format = null;
81   /**
82    * CFType nodes
83    * @var array
84    */
85   protected $value = array();
87   /**
88    * Position of iterator {@link http://php.net/manual/en/class.iterator.php}
89    * @var integer
90    */
91   protected $iteratorPosition = 0;
93   /**
94    * List of Keys for numerical iterator access {@link http://php.net/manual/en/class.iterator.php}
95    * @var array
96    */
97   protected $iteratorKeys = null;
99   /**
100    * List of NodeNames to ClassNames for resolving plist-files
101    * @var array
102    */
103   protected static $types = array(
104     'string'  => 'CFString',
105     'real'    => 'CFNumber',
106     'integer' => 'CFNumber',
107     'date'    => 'CFDate',
108     'true'    => 'CFBoolean',
109     'false'   => 'CFBoolean',
110     'data'    => 'CFData',
111     'array'   => 'CFArray',
112     'dict'    => 'CFDictionary'
113  );
116   /**
117    * Create new CFPropertyList.
118    * If a path to a PropertyList is specified, it is loaded automatically.
119    * @param string $file Path of PropertyList
120    * @param integer $format he format of the property list, see {@link FORMAT_XML}, {@link FORMAT_BINARY} and {@link FORMAT_AUTO}, defaults to {@link FORMAT_AUTO}
121    * @throws IOException if file could not be read by {@link load()}
122    * @uses $file for storing the current file, if specified
123    * @uses load() for loading the plist-file
124    */
125   public function __construct($file=null,$format=self::FORMAT_AUTO) {
126     $this->file = $file;
127     $this->format = $format;
128     $this->detectedFormat = $format;
129     if($this->file) $this->load();
130   }
132   /**
133    * Load an XML PropertyList.
134    * @param string $file Path of PropertyList, defaults to {@link $file}
135    * @return void
136    * @throws IOException if file could not be read
137    * @throws DOMException if XML-file could not be read properly
138    * @uses load() to actually load the file
139    */
140   public function loadXML($file=null) {
141     $this->load($file,CFPropertyList::FORMAT_XML);
142   }
144   /**
145    * Load an XML PropertyList.
146    * @param resource $stream A stream containing the xml document.
147    * @return void
148    * @throws IOException if stream could not be read
149    * @throws DOMException if XML-stream could not be read properly
150    */
151   public function loadXMLStream($stream) {
152     if(($contents = stream_get_contents($stream)) === FALSE) throw IOException::notReadable('<stream>');
153     $this->parse($contents,CFPropertyList::FORMAT_XML);
154   }
156   /**
157    * Load an binary PropertyList.
158    * @param string $file Path of PropertyList, defaults to {@link $file}
159    * @return void
160    * @throws IOException if file could not be read
161    * @throws PListException if binary plist-file could not be read properly
162    * @uses load() to actually load the file
163    */
164   public function loadBinary($file=null) {
165     $this->load($file,CFPropertyList::FORMAT_BINARY);
166   }
168   /**
169    * Load an binary PropertyList.
170    * @param stream $stream Stream containing the PropertyList
171    * @return void
172    * @throws IOException if file could not be read
173    * @throws PListException if binary plist-file could not be read properly
174    * @uses parse() to actually load the file
175    */
176   public function loadBinaryStream($stream) {
177     if(($contents = stream_get_contents($stream)) === FALSE) throw IOException::notReadable('<stream>');
178     $this->parse($contents,CFPropertyList::FORMAT_BINARY);
179   }
181   /**
182    * Load a plist file.
183    * Load and import a plist file.
184    * @param string $file Path of PropertyList, defaults to {@link $file}
185    * @param integer $format The format of the property list, see {@link FORMAT_XML}, {@link FORMAT_BINARY} and {@link FORMAT_AUTO}, defaults to {@link $format}
186    * @return void
187    * @throws PListException if file format version is not 00
188    * @throws IOException if file could not be read
189    * @throws DOMException if plist file could not be parsed properly
190    * @uses $file if argument $file was not specified
191    * @uses $value reset to empty array
192    * @uses import() for importing the values
193    */
194   public function load($file=null,$format=null) {
195     $file = $file ? $file : $this->file;
196     $format = $format !== null ? $format : $this->format;
197     $this->value = array();
199     if(!is_readable($file)) throw IOException::notReadable($file);
201     switch($format) {
202       case CFPropertyList::FORMAT_BINARY:
203         $this->readBinary($file);
204         break;
205       case CFPropertyList::FORMAT_AUTO: // what we now do is ugly, but neccessary to recognize the file format
206         $fd = fopen($file,"rb");
207         if(($magic_number = fread($fd,8)) === false) throw IOException::notReadable($file);
208         fclose($fd);
210         $filetype = substr($magic_number,0,6);
211         $version  = substr($magic_number,-2);
213         if($filetype == "bplist") {
214           if($version != "00") throw new PListException("Wrong file format version! Expected 00, got $version!");
215           $this->detectedFormat = CFPropertyList::FORMAT_BINARY;
216           $this->readBinary($file);
217           break;
218         }
219         $this->detectedFormat = CFPropertyList::FORMAT_XML;
220         // else: xml format, break not neccessary
221       case CFPropertyList::FORMAT_XML:
222         $doc = new DOMDocument();
223         if(!$doc->load($file)) throw new DOMException();
224         $this->import($doc->documentElement, $this);
225         break;
226     }
227   }
229   /**
230    * Parse a plist string.
231    * Parse and import a plist string.
232    * @param string $str String containing the PropertyList, defaults to {@link $content}
233    * @param integer $format The format of the property list, see {@link FORMAT_XML}, {@link FORMAT_BINARY} and {@link FORMAT_AUTO}, defaults to {@link $format}
234    * @return void
235    * @throws PListException if file format version is not 00
236    * @throws IOException if file could not be read
237    * @throws DOMException if plist file could not be parsed properly
238    * @uses $content if argument $str was not specified
239    * @uses $value reset to empty array
240    * @uses import() for importing the values
241    */
242   public function parse($str=NULL,$format=NULL) {
243     $format = $format !== null ? $format : $this->format;
244     $str = $str !== null ? $str : $this->content;
245     $this->value = array();
247     switch($format) {
248       case CFPropertyList::FORMAT_BINARY:
249         $this->parseBinary($str);
250         break;
251       case CFPropertyList::FORMAT_AUTO: // what we now do is ugly, but neccessary to recognize the file format
252         if(($magic_number = substr($str,0,8)) === false) throw IOException::notReadable("<string>");
254         $filetype = substr($magic_number,0,6);
255         $version  = substr($magic_number,-2);
257         if($filetype == "bplist") {
258           if($version != "00") throw new PListException("Wrong file format version! Expected 00, got $version!");
259           $this->detectedFormat = CFPropertyList::FORMAT_BINARY;
260           $this->parseBinary($str);
261           break;
262         }
263         $this->detectedFormat = CFPropertyList::FORMAT_XML;
264         // else: xml format, break not neccessary
265       case CFPropertyList::FORMAT_XML:
266         $doc = new DOMDocument();
267         if(!$doc->loadXML($str)) throw new DOMException();
268         $this->import($doc->documentElement, $this);
269         break;
270     }
271   }
273   /**
274    * Convert a DOMNode into a CFType.
275    * @param DOMNode $node Node to import children of
276    * @param CFDictionary|CFArray|CFPropertyList $parent
277    * @return void
278    */
279   protected function import(DOMNode $node, $parent) {
280     // abort if there are no children
281     if(!$node->childNodes->length) return;
283     foreach($node->childNodes as $n) {
284       // skip if we can't handle the element
285       if(!isset(self::$types[$n->nodeName])) continue;
287       $class = 'CFPropertyList\\'.self::$types[$n->nodeName];
288       $key = null;
290       // find previous <key> if possible
291       $ps = $n->previousSibling;
292       while($ps && $ps->nodeName == '#text' && $ps->previousSibling) $ps = $ps->previousSibling;
294       // read <key> if possible
295       if($ps && $ps->nodeName == 'key') $key = $ps->firstChild->nodeValue;
297       switch($n->nodeName) {
298         case 'date':
299           $value = new $class(CFDate::dateValue($n->nodeValue));
300           break;
301         case 'data':
302           $value = new $class($n->nodeValue,true);
303           break;
304         case 'string':
305           $value = new $class($n->nodeValue);
306           break;
308         case 'real':
309         case 'integer':
310           $value = new $class($n->nodeName == 'real' ? floatval($n->nodeValue) : intval($n->nodeValue));
311           break;
313         case 'true':
314         case 'false':
315           $value = new $class($n->nodeName == 'true');
316           break;
318         case 'array':
319         case 'dict':
320           $value = new $class();
321           $this->import($n, $value);
323           if($value instanceof CFDictionary) {
324             $hsh = $value->getValue();
325             if(isset($hsh['CF$UID']) && count($hsh) == 1) {
326               $value = new CFUid($hsh['CF$UID']->getValue());
327             }
328           }
330           break;
331       }
333       // Dictionaries need a key
334       if($parent instanceof CFDictionary) $parent->add($key, $value);
335       // others don't
336       else $parent->add($value);
337     }
338   }
340   /**
341    * Convert CFPropertyList to XML and save to file.
342    * @param string $file Path of PropertyList, defaults to {@link $file}
343    * @return void
344    * @throws IOException if file could not be read
345    * @uses $file if $file was not specified
346    */
347   public function saveXML($file) {
348     $this->save($file,CFPropertyList::FORMAT_XML);
349   }
351   /**
352    * Convert CFPropertyList to binary format (bplist00) and save to file.
353    * @param string $file Path of PropertyList, defaults to {@link $file}
354    * @return void
355    * @throws IOException if file could not be read
356    * @uses $file if $file was not specified
357    */
358   public function saveBinary($file) {
359     $this->save($file,CFPropertyList::FORMAT_BINARY);
360   }
362   /**
363    * Convert CFPropertyList to XML or binary and save to file.
364    * @param string $file Path of PropertyList, defaults to {@link $file}
365    * @param string $format Format of PropertyList, defaults to {@link $format}
366    * @return void
367    * @throws IOException if file could not be read
368    * @throws PListException if evaluated $format is neither {@link FORMAT_XML} nor {@link FORMAL_BINARY}
369    * @uses $file if $file was not specified
370    * @uses $format if $format was not specified
371    */
372   public function save($file=null,$format=null) {
373     $file = $file ? $file : $this->file;
374     $format = $format ? $format : $this->format;
375     if($format == self::FORMAT_AUTO)$format = $this->detectedFormat;
377     if( !in_array( $format, array( self::FORMAT_BINARY, self::FORMAT_XML ) ) )
378       throw new PListException( "format {$format} is not supported, use CFPropertyList::FORMAT_BINARY or CFPropertyList::FORMAT_XML" );
380     if(!file_exists($file)) {
381       // dirname("file.xml") == "" and is treated as the current working directory
382       if(!is_writable(dirname($file))) throw IOException::notWritable($file);
383     }
384     else if(!is_writable($file)) throw IOException::notWritable($file);
386     $content = $format == self::FORMAT_BINARY ? $this->toBinary() : $this->toXML();
388     $fh = fopen($file, 'wb');
389     fwrite($fh,$content);
390     fclose($fh);
391   }
393   /**
394    * Convert CFPropertyList to XML
395    * @param bool $formatted Print plist formatted (i.e. with newlines and whitespace indention) if true; defaults to false
396    * @return string The XML content
397    */
398   public function toXML($formatted=false) {
399     $domimpl = new DOMImplementation();
400     // <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
401     $dtd = $domimpl->createDocumentType('plist', '-//Apple//DTD PLIST 1.0//EN', 'http://www.apple.com/DTDs/PropertyList-1.0.dtd');
402     $doc = $domimpl->createDocument(null, "plist", $dtd);
403     $doc->encoding = "UTF-8";
405     // format output
406     if($formatted) {
407       $doc->formatOutput = true;
408       $doc->preserveWhiteSpace = true;
409     }
411     // get documentElement and set attribs
412     $plist = $doc->documentElement;
413     $plist->setAttribute('version', '1.0');
415     // add PropertyList's children
416     $plist->appendChild($this->getValue(true)->toXML($doc));
418     return $doc->saveXML();
419   }
422   /************************************************************************************************
423    *    M A N I P U L A T I O N
424    ************************************************************************************************/
426   /**
427    * Add CFType to collection.
428    * @param CFType $value CFType to add to collection
429    * @return void
430    * @uses $value for adding $value
431    */
432   public function add(CFType $value=null) {
433     // anything but CFType is null, null is an empty string - sad but true
434     if( !$value )
435       $value = new CFString();
437     $this->value[] = $value;
438   }
440   /**
441    * Get CFType from collection.
442    * @param integer $key Key of CFType to retrieve from collection
443    * @return CFType CFType found at $key, null else
444    * @uses $value for retrieving CFType of $key
445    */
446   public function get($key) {
447     if(isset($this->value[$key])) return $this->value[$key];
448     return null;
449   }
451   /**
452    * Generic getter (magic)
453    *
454    * @param integer $key Key of CFType to retrieve from collection
455    * @return CFType CFType found at $key, null else
456    * @author Sean Coates <sean@php.net>
457    * @link http://php.net/oop5.overloading
458    */
459   public function __get($key) {
460     return $this->get($key);
461   }
463   /**
464    * Remove CFType from collection.
465    * @param integer $key Key of CFType to removes from collection
466    * @return CFType removed CFType, null else
467    * @uses $value for removing CFType of $key
468    */
469   public function del($key) {
470     if(isset($this->value[$key])) {
471       $t = $this->value[$key];
472       unset($this->value[$key]);
473       return $t;
474     }
476     return null;
477   }
479   /**
480    * Empty the collection
481    * @return array the removed CFTypes
482    * @uses $value for removing CFType of $key
483    */
484   public function purge() {
485     $t = $this->value;
486     $this->value = array();
487     return $t;
488   }
490   /**
491    * Get first (and only) child, or complete collection.
492    * @param string $cftype if set to true returned value will be CFArray instead of an array in case of a collection
493    * @return CFType|array CFType or list of CFTypes known to the PropertyList
494    * @uses $value for retrieving CFTypes
495    */
496   public function getValue($cftype=false) {
497     if(count($this->value) === 1) {
498       $t = array_values( $this->value );
499       return $t[0];
500         }
501     if($cftype) {
502       $t = new CFArray();
503       foreach( $this->value as $value ) {
504         if( $value instanceof CFType ) $t->add($value);
505       }
506       return $t;
507     }
508     return $this->value;
509   }
511   /**
512    * Create CFType-structure from guessing the data-types.
513    * The functionality has been moved to the more flexible {@link CFTypeDetector} facility.
514    * @param mixed $value Value to convert to CFType
515    * @param array $options Configuration for casting values [autoDictionary, suppressExceptions, objectToArrayMethod, castNumericStrings]
516    * @return CFType CFType based on guessed type
517    * @uses CFTypeDetector for actual type detection
518    * @deprecated
519    */
520   public static function guess($value, $options=array()) {
521     static $t = null;
522     if( $t === null )
523       $t = new CFTypeDetector( $options );
525     return $t->toCFType( $value );
526   }
529   /************************************************************************************************
530    *    S E R I A L I Z I N G
531    ************************************************************************************************/
533   /**
534    * Get PropertyList as array.
535    * @return mixed primitive value of first (and only) CFType, or array of primitive values of collection
536    * @uses $value for retrieving CFTypes
537    */
538   public function toArray() {
539     $a = array();
540     foreach($this->value as $value) $a[] = $value->toArray();
541     if(count($a) === 1) return $a[0];
543     return $a;
544   }
547   /************************************************************************************************
548    *    I T E R A T O R   I N T E R F A C E
549    ************************************************************************************************/
551   /**
552    * Rewind {@link $iteratorPosition} to first position (being 0)
553    * @link http://php.net/manual/en/iterator.rewind.php
554    * @return void
555    * @uses $iteratorPosition set to 0
556    * @uses $iteratorKeys store keys of {@link $value}
557    */
558   public function rewind() {
559     $this->iteratorPosition = 0;
560     $this->iteratorKeys = array_keys($this->value);
561   }
563   /**
564    * Get Iterator's current {@link CFType} identified by {@link $iteratorPosition}
565    * @link http://php.net/manual/en/iterator.current.php
566    * @return CFType current Item
567    * @uses $iteratorPosition identify current key
568    * @uses $iteratorKeys identify current value
569    */
570   public function current() {
571     return $this->value[$this->iteratorKeys[$this->iteratorPosition]];
572   }
574   /**
575    * Get Iterator's current key identified by {@link $iteratorPosition}
576    * @link http://php.net/manual/en/iterator.key.php
577    * @return string key of the current Item
578    * @uses $iteratorPosition identify current key
579    * @uses $iteratorKeys identify current value
580    */
581   public function key() {
582     return $this->iteratorKeys[$this->iteratorPosition];
583   }
585   /**
586    * Increment {@link $iteratorPosition} to address next {@see CFType}
587    * @link http://php.net/manual/en/iterator.next.php
588    * @return void
589    * @uses $iteratorPosition increment by 1
590    */
591   public function next() {
592     $this->iteratorPosition++;
593   }
595   /**
596    * Test if {@link $iteratorPosition} addresses a valid element of {@link $value}
597    * @link http://php.net/manual/en/iterator.valid.php
598    * @return boolean true if current position is valid, false else
599    * @uses $iteratorPosition test if within {@link $iteratorKeys}
600    * @uses $iteratorPosition test if within {@link $value}
601    */
602   public function valid() {
603     return isset($this->iteratorKeys[$this->iteratorPosition]) && isset($this->value[$this->iteratorKeys[$this->iteratorPosition]]);
604   }
608 # eof