lib_opml.php 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. <?php
  2. /**
  3. * lib_opml is a free library to manage OPML format in PHP.
  4. *
  5. * By default, it takes in consideration version 2.0 but can be compatible with
  6. * OPML 1.0. More information on http://dev.opml.org.
  7. * Difference is "text" attribute is optional in version 1.0. It is highly
  8. * recommended to use this attribute.
  9. *
  10. * lib_opml requires SimpleXML (php.net/simplexml) and DOMDocument (php.net/domdocument)
  11. *
  12. * @author Marien Fressinaud <dev@marienfressinaud.fr>
  13. * @link https://github.com/marienfressinaud/lib_opml
  14. * @version 0.2
  15. * @license public domain
  16. *
  17. * Usages:
  18. * > include('lib_opml.php');
  19. * > $filename = 'my_opml_file.xml';
  20. * > $opml_array = libopml_parse_file($filename);
  21. * > print_r($opml_array);
  22. *
  23. * > $opml_string = [...];
  24. * > $opml_array = libopml_parse_string($opml_string);
  25. * > print_r($opml_array);
  26. *
  27. * > $opml_array = [...];
  28. * > $opml_string = libopml_render($opml_array);
  29. * > $opml_object = libopml_render($opml_array, true);
  30. * > echo $opml_string;
  31. * > print_r($opml_object);
  32. *
  33. * You can set $strict argument to false if you want to bypass "text" attribute
  34. * requirement.
  35. *
  36. * If parsing fails for any reason (e.g. not an XML string, does not match with
  37. * the specifications), a LibOPML_Exception is raised.
  38. *
  39. * lib_opml array format is described here:
  40. * $array = array(
  41. * 'head' => array( // 'head' element is optional (but recommended)
  42. * 'key' => 'value', // key must be a part of available OPML head elements
  43. * ),
  44. * 'body' => array( // body is required
  45. * array( // this array represents an outline (at least one)
  46. * 'text' => 'value', // 'text' element is required if $strict is true
  47. * 'key' => 'value', // key and value are what you want (optional)
  48. * '@outlines' = array( // @outlines is a special value and represents sub-outlines
  49. * array(
  50. * [...] // where [...] is a valid outline definition
  51. * ),
  52. * ),
  53. * ),
  54. * array( // other outline definitions
  55. * [...]
  56. * ),
  57. * [...],
  58. * )
  59. * )
  60. *
  61. */
  62. /**
  63. * A simple Exception class which represents any kind of OPML problem.
  64. * Message should precise the current problem.
  65. */
  66. class LibOPML_Exception extends Exception {}
  67. // Define the list of available head attributes. All of them are optional.
  68. define('HEAD_ELEMENTS', serialize(array(
  69. 'title', 'dateCreated', 'dateModified', 'ownerName', 'ownerEmail',
  70. 'ownerId', 'docs', 'expansionState', 'vertScrollState', 'windowTop',
  71. 'windowLeft', 'windowBottom', 'windowRight'
  72. )));
  73. /**
  74. * Parse an XML object as an outline object and return corresponding array
  75. *
  76. * @param SimpleXMLElement $outline_xml the XML object we want to parse
  77. * @param bool $strict true if "text" attribute is required, false else
  78. * @return array corresponding to an outline and following format described above
  79. * @throws LibOPML_Exception
  80. * @access private
  81. */
  82. function libopml_parse_outline($outline_xml, $strict = true) {
  83. $outline = array();
  84. // An outline may contain any kind of attributes but "text" attribute is
  85. // required !
  86. $text_is_present = false;
  87. foreach ($outline_xml->attributes() as $key => $value) {
  88. $outline[$key] = (string)$value;
  89. if ($key === 'text') {
  90. $text_is_present = true;
  91. }
  92. }
  93. if (!$text_is_present && $strict) {
  94. throw new LibOPML_Exception(
  95. 'Outline does not contain any text attribute'
  96. );
  97. }
  98. foreach ($outline_xml->children() as $key => $value) {
  99. // An outline may contain any number of outline children
  100. if ($key === 'outline') {
  101. $outline['@outlines'][] = libopml_parse_outline($value, $strict);
  102. } else {
  103. throw new LibOPML_Exception(
  104. 'Body can contain only outline elements'
  105. );
  106. }
  107. }
  108. return $outline;
  109. }
  110. /**
  111. * Parse a string as a XML one and returns the corresponding array
  112. *
  113. * @param string $xml is the string we want to parse
  114. * @param bool $strict true if "text" attribute is required, false else
  115. * @return array corresponding to the XML string and following format described above
  116. * @throws LibOPML_Exception
  117. * @access public
  118. */
  119. function libopml_parse_string($xml, $strict = true) {
  120. $dom = new DOMDocument();
  121. $dom->recover = true;
  122. $dom->strictErrorChecking = false;
  123. $dom->loadXML($xml);
  124. $dom->encoding = 'UTF-8';
  125. $opml = simplexml_import_dom($dom);
  126. if (!$opml) {
  127. throw new LibOPML_Exception();
  128. }
  129. $array = array(
  130. 'version' => (string)$opml['version'],
  131. 'head' => array(),
  132. 'body' => array()
  133. );
  134. // First, we get all "head" elements. Head is required but its sub-elements
  135. // are optional.
  136. foreach ($opml->head->children() as $key => $value) {
  137. if (in_array($key, unserialize(HEAD_ELEMENTS), true)) {
  138. $array['head'][$key] = (string)$value;
  139. } else {
  140. throw new LibOPML_Exception(
  141. $key . 'is not part of OPML format'
  142. );
  143. }
  144. }
  145. // Then, we get body oulines. Body must contain at least one outline
  146. // element.
  147. $at_least_one_outline = false;
  148. foreach ($opml->body->children() as $key => $value) {
  149. if ($key === 'outline') {
  150. $at_least_one_outline = true;
  151. $array['body'][] = libopml_parse_outline($value, $strict);
  152. } else {
  153. throw new LibOPML_Exception(
  154. 'Body can contain only outline elements'
  155. );
  156. }
  157. }
  158. if (!$at_least_one_outline) {
  159. throw new LibOPML_Exception(
  160. 'Body must contain at least one outline element'
  161. );
  162. }
  163. return $array;
  164. }
  165. /**
  166. * Parse a string contained into a file as a XML string and returns the corresponding array
  167. *
  168. * @param string $filename should indicates a valid XML file
  169. * @param bool $strict true if "text" attribute is required, false else
  170. * @return array corresponding to the file content and following format described above
  171. * @throws LibOPML_Exception
  172. * @access public
  173. */
  174. function libopml_parse_file($filename, $strict = true) {
  175. $file_content = file_get_contents($filename);
  176. if ($file_content === false) {
  177. throw new LibOPML_Exception(
  178. $filename . ' cannot be found'
  179. );
  180. }
  181. return libopml_parse_string($file_content, $strict);
  182. }
  183. /**
  184. * Create a XML outline object in a parent object.
  185. *
  186. * @param SimpleXMLElement $parent_elt is the parent object of current outline
  187. * @param array $outline array representing an outline object
  188. * @param bool $strict true if "text" attribute is required, false else
  189. * @throws LibOPML_Exception
  190. * @access private
  191. */
  192. function libopml_render_outline($parent_elt, $outline, $strict) {
  193. // Outline MUST be an array!
  194. if (!is_array($outline)) {
  195. throw new LibOPML_Exception(
  196. 'Outline element must be defined as array'
  197. );
  198. }
  199. $outline_elt = $parent_elt->addChild('outline');
  200. $text_is_present = false;
  201. foreach ($outline as $key => $value) {
  202. // Only outlines can be an array and so we consider children are also
  203. // outline elements.
  204. if ($key === '@outlines' && is_array($value)) {
  205. foreach ($value as $outline_child) {
  206. libopml_render_outline($outline_elt, $outline_child, $strict);
  207. }
  208. } elseif (is_array($value)) {
  209. throw new LibOPML_Exception(
  210. 'Type of outline elements cannot be array: ' . $key
  211. );
  212. } else {
  213. // Detect text attribute is present, that's good :)
  214. if ($key === 'text') {
  215. $text_is_present = true;
  216. }
  217. $outline_elt->addAttribute($key, $value);
  218. }
  219. }
  220. if (!$text_is_present && $strict) {
  221. throw new LibOPML_Exception(
  222. 'You must define at least a text element for all outlines'
  223. );
  224. }
  225. }
  226. /**
  227. * Render an array as an OPML string or a XML object.
  228. *
  229. * @param array $array is the array we want to render and must follow structure defined above
  230. * @param bool $as_xml_object false if function must return a string, true for a XML object
  231. * @param bool $strict true if "text" attribute is required, false else
  232. * @return string|SimpleXMLElement XML string corresponding to $array or XML object
  233. * @throws LibOPML_Exception
  234. * @access public
  235. */
  236. function libopml_render($array, $as_xml_object = false, $strict = true) {
  237. $opml = new SimpleXMLElement('<opml></opml>');
  238. $opml->addAttribute('version', $strict ? '2.0' : '1.0');
  239. // Create head element. $array['head'] is optional but head element will
  240. // exist in the final XML object.
  241. $head = $opml->addChild('head');
  242. if (isset($array['head'])) {
  243. foreach ($array['head'] as $key => $value) {
  244. if (in_array($key, unserialize(HEAD_ELEMENTS), true)) {
  245. $head->addChild($key, $value);
  246. }
  247. }
  248. }
  249. // Check body is set and contains at least one element
  250. if (!isset($array['body'])) {
  251. throw new LibOPML_Exception(
  252. '$array must contain a body element'
  253. );
  254. }
  255. if (count($array['body']) <= 0) {
  256. throw new LibOPML_Exception(
  257. 'Body element must contain at least one element (array)'
  258. );
  259. }
  260. // Create outline elements
  261. $body = $opml->addChild('body');
  262. foreach ($array['body'] as $outline) {
  263. libopml_render_outline($body, $outline, $strict);
  264. }
  265. // And return the final result
  266. if ($as_xml_object) {
  267. return $opml;
  268. } else {
  269. $dom = dom_import_simplexml($opml)->ownerDocument;
  270. $dom->formatOutput = true;
  271. $dom->encoding = 'UTF-8';
  272. return $dom->saveXML();
  273. }
  274. }