1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12:
13:
14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27:
28: class Horde_Pear_Package_Xml
29: {
30:
31: const XMLNAMESPACE = 'http://pear.php.net/dtd/package-2.0';
32:
33: 34: 35: 36: 37:
38: private $_xml;
39:
40: 41: 42: 43: 44:
45: private $_path;
46:
47: 48: 49: 50: 51:
52: private $_xpath;
53:
54: 55: 56: 57: 58:
59: private $_namespace_prefix = '';
60:
61: 62: 63: 64: 65:
66: private $_factory;
67:
68: 69: 70: 71: 72:
73: public function __construct($xml, $factory = null)
74: {
75: if (is_resource($xml)) {
76: rewind($xml);
77: } else {
78: $this->_path = $xml;
79: $xml = fopen($xml, 'r');
80: }
81: $this->_xml = new DOMDocument('1.0', 'UTF-8');
82: $this->_xml->loadXML(stream_get_contents($xml));
83: $rootNamespace = $this->_xml->lookupNamespaceUri($this->_xml->namespaceURI);
84: $this->_xpath = new DOMXpath($this->_xml);
85: if ($rootNamespace !== null) {
86: $this->_xpath->registerNamespace('p', $rootNamespace);
87: $this->_namespace_prefix = 'p:';
88: }
89: if ($factory === null) {
90: $this->_factory = new Horde_Pear_Package_Xml_Factory();
91: } else {
92: $this->_factory = $factory;
93: }
94: }
95:
96: 97: 98: 99: 100:
101: public function getContent($type = 'horde', $path = null)
102: {
103: if ($path === null) {
104: if ($this->_path === null) {
105: throw new Horde_Pear_Exception('The path has not been provided!');
106: }
107: $path = $this->_path;
108: }
109: if (!is_object($type)) {
110: if ($type == 'horde' || !class_exists($type)) {
111: $type = 'Horde_Pear_Package_Type_' . ucfirst($type);
112: }
113: $type = new $type(dirname($this->_path));
114: }
115: return new Horde_Pear_Package_Contents_List($type);
116: }
117:
118: 119: 120: 121: 122:
123: public function getName()
124: {
125: return $this->getNodeText('/p:package/p:name');
126: }
127:
128: 129: 130: 131: 132:
133: public function getChannel()
134: {
135: return $this->getNodeText('/p:package/p:channel');
136: }
137:
138: 139: 140: 141: 142:
143: public function getSummary()
144: {
145: return $this->getNodeText('/p:package/p:summary');
146: }
147:
148: 149: 150: 151: 152:
153: public function getDescription()
154: {
155: return $this->getNodeText('/p:package/p:description');
156: }
157:
158: 159: 160: 161: 162:
163: public function getVersion()
164: {
165: return $this->getNodeText('/p:package/p:version/p:release');
166: }
167:
168: 169: 170: 171: 172:
173: public function getDate()
174: {
175: return $this->getNodeText('/p:package/p:date');
176: }
177:
178: 179: 180: 181: 182:
183: public function getNotes()
184: {
185: return $this->getNodeText('/p:package/p:notes');
186: }
187:
188: 189: 190: 191: 192: 193: 194:
195: public function getState($key = 'release')
196: {
197: if (in_array($key, array('release', 'api'))) {
198: return $this->getNodeText('/p:package/p:stability/p:' . $key);
199: }
200: throw new Horde_Pear_Exception(sprintf('Unsupported state "%s"!', $key));
201: }
202:
203: 204: 205: 206: 207:
208: public function getDependencies()
209: {
210: $result = array();
211: $this->_completeDependencies(
212: $this->findNode('/p:package/p:dependencies/p:required'),
213: $result,
214: 'no'
215: );
216: $this->_completeDependencies(
217: $this->findNode('/p:package/p:dependencies/p:optional'),
218: $result,
219: 'yes'
220: );
221: return $result;
222: }
223:
224: 225: 226: 227: 228: 229: 230: 231: 232:
233: private function _completeDependencies($parent, &$result, $optional)
234: {
235: if ($parent === false) {
236: return;
237: }
238: foreach ($parent->childNodes as $dep) {
239: if ($dep->nodeType == XML_TEXT_NODE) {
240: continue;
241: }
242: $input = array();
243: $this->_dependencyInputValue($input, 'min', $dep);
244: $this->_dependencyInputValue($input, 'max', $dep);
245: $this->_dependencyInputValue($input, 'name', $dep);
246: $this->_dependencyInputValue($input, 'channel', $dep);
247: $this->_dependencyInputValue($input, 'conflicts', $dep);
248: Horde_Pear_Package_Dependencies::addDependency(
249: $input, $dep->nodeName, $optional, $result
250: );
251: }
252: }
253:
254: 255: 256: 257: 258: 259: 260: 261: 262:
263: private function _dependencyInputValue(&$input, $name, $node)
264: {
265: if (($result = $this->getNodeTextRelativeTo('./p:' . $name, $node)) !== false) {
266: $input[$name] = $result;
267: }
268: }
269:
270: 271: 272: 273: 274:
275: public function getLicense()
276: {
277: return $this->getNodeText('/p:package/p:license');
278: }
279:
280: 281: 282: 283: 284:
285: public function getLicenseLocation()
286: {
287: $node = $this->findNode('/p:package/p:license');
288: if (empty($node)) {
289: return '';
290: }
291: return $node->getAttribute('uri');
292: }
293:
294: 295: 296: 297: 298:
299: public function getLeads()
300: {
301: $result = array();
302: foreach ($this->findNodes('/p:package/p:lead') as $lead) {
303: $result[] = array(
304: 'name' => $this->getNodeTextRelativeTo('./p:name', $lead),
305: 'user' => $this->getNodeTextRelativeTo('./p:user', $lead),
306: 'email' => $this->getNodeTextRelativeTo('./p:email', $lead),
307: 'active' => $this->getNodeTextRelativeTo('./p:active', $lead),
308: );
309: }
310: return $result;
311: }
312:
313: 314: 315: 316: 317: 318: 319: 320:
321: public function __call($name, $arguments)
322: {
323: if (substr($name, 0, 6) == 'create') {
324: return $this->_factory->create(substr($name, 6), $arguments);
325: } else {
326: array_unshift($arguments, $this);
327: $this->_factory->createTask($name, $arguments)->run();
328: }
329: }
330:
331: 332: 333: 334: 335:
336: public function timestamp()
337: {
338: $this->replaceTextNode('/p:package/p:date', date('Y-m-d'));
339: $this->replaceTextNode('/p:package/p:time', date('H:i:s'));
340:
341: $release = $this->_requireCurrentRelease();
342:
343: $this->replaceTextNodeRelativeTo('./p:date', $release, date('Y-m-d'));
344: }
345:
346: 347: 348: 349: 350: 351:
352: public function syncCurrentVersion()
353: {
354: $date = $this->getNodeText('/p:package/p:date');
355: $notes = $this->getNodeText('/p:package/p:notes');
356: $api = $this->getNodeText('/p:package/p:version/p:api');
357: $stability_api = $this->getNodeText('/p:package/p:stability/p:api');
358: $stability_release = $this->getNodeText(
359: '/p:package/p:stability/p:release'
360: );
361:
362: $release = $this->_requireCurrentRelease();
363:
364: $this->replaceTextNodeRelativeTo('./p:date', $release, $date);
365: $this->replaceTextNodeRelativeTo(
366: './p:notes', $release, $notes . ' '
367: );
368: $this->replaceTextNodeRelativeTo(
369: './p:license',
370: $release,
371: $this->getLicense(),
372: array('uri' => $this->getLicenseLocation())
373: );
374: $version_node = $this->findNodeRelativeTo(
375: './p:version', $release
376: );
377: $this->replaceTextNodeRelativeTo(
378: './p:api', $version_node, $api
379: );
380: $stability_node = $this->findNodeRelativeTo(
381: './p:stability', $release
382: );
383: $this->replaceTextNodeRelativeTo(
384: './p:api', $stability_node, $stability_api
385: );
386: $this->replaceTextNodeRelativeTo(
387: './p:release', $stability_node, $stability_release
388: );
389: }
390:
391: 392: 393: 394: 395: 396: 397:
398: public function addNote($note)
399: {
400: $notes = trim($this->getNodeText('/p:package/p:notes'));
401: if ($notes != '*') {
402: $new_notes = "\n* " . $note . "\n" . $notes . "\n ";
403: } else {
404: $new_notes = "\n* " . $note . "\n ";
405: }
406: $this->replaceTextNode('/p:package/p:notes', $new_notes);
407:
408: $release = $this->_fetchCurrentRelease();
409: if ($release !== null) {
410: $this->replaceTextNodeRelativeTo(
411: './p:notes', $release, $new_notes . ' '
412: );
413: }
414: }
415:
416: 417: 418: 419: 420: 421: 422: 423:
424: public function setVersion($rel_version = null, $api_version = null)
425: {
426: $release = $this->findNodeRelativeTo(
427: './p:version',
428: $this->_requireCurrentRelease()
429: );
430: $version = $this->findNode('/p:package/p:version');
431: if ($rel_version !== null) {
432: $this->replaceTextNodeRelativeTo(
433: './p:release', $version, $rel_version
434: );
435: $this->replaceTextNodeRelativeTo(
436: './p:release', $release, $rel_version
437: );
438: }
439: if ($api_version !== null) {
440: $this->replaceTextNodeRelativeTo(
441: './p:api', $version, $api_version
442: );
443: $this->replaceTextNodeRelativeTo(
444: './p:api', $release, $api_version
445: );
446: }
447: }
448:
449: 450: 451: 452: 453: 454: 455: 456: 457: 458:
459: public function addNextVersion($version, $initial_note,
460: $stability_api = null,
461: $stability_release = null)
462: {
463: $notes = "\n* " . $initial_note . "\n ";
464: $api = $this->getNodeText('/p:package/p:version/p:api');
465: if ($stability_api === null) {
466: $stability_api = $this->getNodeText('/p:package/p:stability/p:api');
467: }
468: if ($stability_release === null) {
469: $stability_release = $this->getNodeText(
470: '/p:package/p:stability/p:release'
471: );
472: }
473: $version_node = $this->findNode('/p:package/p:version');
474: $this->replaceTextNodeRelativeTo(
475: './p:release', $version_node, $version
476: );
477: $this->replaceTextNode('/p:package/p:notes', $notes);
478: $this->replaceTextNode('/p:package/p:date', date('Y-m-d'));
479: $this->replaceTextNode('/p:package/p:time', date('H:i:s'));
480:
481: $changelog = $this->findNode('/p:package/p:changelog');
482: $this->_insertWhiteSpace($changelog, ' ');
483:
484: $release = $this->_xml->createElementNS(self::XMLNAMESPACE, 'release');
485: $this->_appendVersion($release, $version, $api, "\n ");
486: $this->_appendStability($release, $stability_release, $stability_api, "\n ");
487: $this->_appendChild($release, 'date', date('Y-m-d'), "\n ");
488: $this->_appendLicense(
489: $release,
490: $this->getLicense(),
491: $this->getLicenseLocation(),
492: "\n "
493: );
494: $this->_appendChild($release, 'notes', $notes . ' ', "\n ");
495: $this->_insertWhiteSpace($release, "\n ");
496: $changelog->appendChild($release);
497: $this->_insertWhiteSpace($changelog, "\n ");
498: }
499:
500: 501: 502: 503: 504: 505: 506: 507: 508: 509:
510: private function _appendVersion($parent, $version, $api, $ws = '')
511: {
512: $this->_insertWhiteSpace($parent, $ws);
513: $node = $this->_xml->createElementNS(self::XMLNAMESPACE, 'version');
514: $this->_appendChild($node, 'release', $version, "\n ");
515: $this->_appendChild($node, 'api', $api, "\n ");
516: $parent->appendChild($node);
517: }
518:
519: 520: 521: 522: 523: 524: 525: 526: 527: 528:
529: private function _appendStability($parent, $release, $api, $ws = null)
530: {
531: $this->_insertWhiteSpace($parent, $ws);
532: $node = $this->_xml->createElementNS(self::XMLNAMESPACE, 'stability');
533: $this->_appendChild($node, 'release', $release, "\n ");
534: $this->_appendChild($node, 'api', $api, "\n ");
535: $parent->appendChild($node);
536: }
537:
538: 539: 540: 541: 542: 543: 544: 545: 546: 547:
548: private function _appendLicense($parent, $license, $uri, $ws = null)
549: {
550: $this->_insertWhiteSpace($parent, $ws);
551: $new_node = $this->_xml->createElementNS(
552: self::XMLNAMESPACE, 'license'
553: );
554: $text = $this->_xml->createTextNode($license);
555: $new_node->appendChild($text);
556: $new_node->setAttribute('uri', $uri);
557: $parent->appendChild($new_node);
558: }
559:
560: 561: 562: 563: 564: 565: 566: 567:
568: private function _requireCurrentRelease()
569: {
570: $release = $this->_fetchCurrentRelease();
571: if ($release === null) {
572: throw new Horde_Pear_Exception('No release in the changelog matches the current version!');
573: }
574: return $release;
575: }
576:
577: 578: 579: 580: 581:
582: private function _fetchCurrentRelease()
583: {
584: $version = $this->getNodeText('/p:package/p:version/p:release');
585: foreach ($this->findNodes('/p:package/p:changelog/p:release') as $release) {
586: if ($this->getNodeTextRelativeTo('./p:version/p:release', $release) == $version) {
587: return $release;
588: }
589: }
590: }
591:
592: 593: 594: 595: 596:
597: public function __toString()
598: {
599: $result = $this->_xml->saveXML();
600: $result = preg_replace(
601: '#<package (.*) (packagerversion="[.0-9]*" version="2.0")#',
602: '<package \2 \1',
603: $result
604: );
605: $result = preg_replace("#'#", ''', $result);
606: return preg_replace('#"/>#', '" />', $result);
607: }
608:
609: 610: 611: 612: 613: 614: 615:
616: public function findNode($query)
617: {
618: return $this->_findSingleNode($this->findNodes($query));
619: }
620:
621: 622: 623: 624: 625: 626: 627: 628: 629:
630: public function findNodeRelativeTo($query, DOMNode $context)
631: {
632: return $this->_findSingleNode(
633: $this->findNodesRelativeTo($query, $context)
634: );
635: }
636:
637: 638: 639: 640: 641: 642: 643:
644: private function _findSingleNode($result)
645: {
646: if ($result->length) {
647: return $result->item(0);
648: }
649: return false;
650: }
651:
652: 653: 654: 655: 656: 657: 658:
659: public function findNodes($query)
660: {
661: $query = preg_replace('#/p:#', '/' . $this->_namespace_prefix, $query);
662: return $this->_xpath->query($query);
663: }
664:
665: 666: 667: 668: 669: 670: 671:
672: public function findNodesRelativeTo($query, $context)
673: {
674: $query = preg_replace('#/p:#', '/' . $this->_namespace_prefix, $query);
675: return $this->_xpath->query($query, $context);
676: }
677:
678: 679: 680: 681: 682: 683: 684: 685:
686: public function getNodeText($path)
687: {
688: if ($node = $this->findNode($path)) {
689: return $node->textContent;
690: }
691: return false;
692: }
693:
694: 695: 696: 697: 698: 699: 700: 701: 702: 703:
704: public function getNodeTextRelativeTo($path, DOMNode $context)
705: {
706: if ($node = $this->findNodeRelativeTo($path, $context)) {
707: return $node->textContent;
708: }
709: return false;
710: }
711:
712: 713: 714: 715: 716: 717: 718: 719:
720: public function replaceTextNode($path, $value)
721: {
722: if ($node = $this->findNode($path)) {
723: $this->_xml->documentElement->replaceChild(
724: $this->_replacementNode($node, $value), $node
725: );
726: }
727: }
728:
729: 730: 731: 732: 733: 734: 735: 736: 737: 738:
739: public function replaceTextNodeRelativeTo($path, DOMNode $context, $value,
740: $attributes = array())
741: {
742: if ($node = $this->findNodeRelativeTo($path, $context)) {
743: $new_node = $this->_replacementNode($node, $value);
744: foreach ($attributes as $name => $value) {
745: $new_node->setAttribute($name, $value);
746: }
747: $context->replaceChild($new_node, $node);
748: }
749: }
750:
751: 752: 753: 754: 755: 756: 757: 758:
759: private function _replacementNode($old_node, $value)
760: {
761: $new_node = $this->_xml->createElementNS(
762: self::XMLNAMESPACE, $old_node->tagName
763: );
764: $text = $this->_xml->createTextNode($value);
765: $new_node->appendChild($text);
766: return $new_node;
767: }
768:
769: 770: 771: 772: 773: 774: 775: 776: 777: 778:
779: private function _appendChild($parent, $name, $value, $ws = '')
780: {
781: $this->_insertWhiteSpace($parent, $ws);
782: $new_node = $this->_xml->createElementNS(
783: self::XMLNAMESPACE, $name
784: );
785: $text = $this->_xml->createTextNode($value);
786: $new_node->appendChild($text);
787: $parent->appendChild($new_node);
788: }
789:
790: 791: 792: 793: 794: 795: 796: 797:
798: public function _insertWhiteSpace($parent, $ws)
799: {
800: $ws_node = $this->_xml->createTextNode($ws);
801: $parent->appendChild($ws_node);
802: return $ws_node;
803: }
804:
805:
806: public function insert($elements, $point)
807: {
808: if (!is_array($elements)) {
809: $elements = array($elements);
810: }
811: $node = null;
812: foreach ($elements as $key => $element) {
813: if (is_string($element)) {
814: $element = $this->createText($element);
815: } else if (is_array($element)) {
816: $node = $element = $this->createNode($key, $element);
817: }
818: $point->parentNode->insertBefore($element, $point);
819: }
820: return $node;
821: }
822:
823: public function append($elements, $parent)
824: {
825: if (!is_array($elements)) {
826: $elements = array($elements);
827: }
828: $node = null;
829: foreach ($elements as $key => $element) {
830: if (is_string($element)) {
831: $element = $this->createText($element);
832: } else if (is_array($element)) {
833: $node = $element = $this->createNode($key, $element);
834: }
835: $parent->appendChild($element);
836: }
837: return $node;
838: }
839:
840: public function createText($text)
841: {
842: return $this->_xml->createTextNode($text);
843: }
844:
845: public function ($comment)
846: {
847: return $this->_xml->createComment($comment);
848: }
849:
850: public function createNode($name, $attributes = array())
851: {
852: $node = $this->_xml->createElementNS(self::XMLNAMESPACE, $name);
853: foreach ($attributes as $key => $value) {
854: $node->setAttribute($key, $value);
855: }
856: return $node;
857: }
858:
859: public function removeWhitespace($node)
860: {
861: if ($node) {
862: $ws = trim($node->textContent);
863: if (empty($ws)) {
864: $node->parentNode->removeChild($node);
865: }
866: }
867: }
868:
869: public function ($node, $comment)
870: {
871: if ($node) {
872: $current = trim($node->textContent);
873: if ($current == $comment) {
874: $node->parentNode->removeChild($node);
875: }
876: }
877: }
878: }