CFPropertyList.php 23 KB

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