LibOpml.php 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770
  1. <?php
  2. namespace marienfressinaud\LibOpml;
  3. /**
  4. * The LibOpml class provides the methods to read and write OPML files and
  5. * strings. It transforms OPML files or strings to PHP arrays (or the reverse).
  6. *
  7. * How to read this file?
  8. *
  9. * The first methods are dedicated to the parsing, and the next ones to the
  10. * reading. The three last methods are helpful methods, but you don't have to
  11. * worry too much about them.
  12. *
  13. * The main methods are the public ones: parseFile, parseString and render.
  14. * They call the other parse* and render* methods internally.
  15. *
  16. * These three main methods are available as functions (see the src/functions.php
  17. * file).
  18. *
  19. * What's the array format?
  20. *
  21. * As said before, LibOpml transforms OPML to PHP arrays, or the reverse. The
  22. * format is pretty simple. It contains four keys:
  23. *
  24. * - version: the version of the OPML;
  25. * - namespaces: an array of namespaces used in the OPML, if any;
  26. * - head: an array of OPML head elements, where keys are the names of the
  27. * elements;
  28. * - body: an array of arrays representing OPML outlines, where keys are the
  29. * name of the attributes (the special @outlines key contains the sub-outlines).
  30. *
  31. * When rendering, only the body key is required (version will default to 2.0).
  32. *
  33. * Example:
  34. *
  35. * [
  36. * version => '2.0',
  37. * namespaces => [],
  38. * head => [
  39. * title => 'An OPML file'
  40. * ],
  41. * body => [
  42. * [
  43. * text => 'Newspapers',
  44. * @outlines => [
  45. * [text => 'El País'],
  46. * [text => 'Le Monde'],
  47. * [text => 'The Guardian'],
  48. * [text => 'The New York Times'],
  49. * ]
  50. * ]
  51. * ]
  52. * ]
  53. *
  54. * @see http://opml.org/spec2.opml
  55. *
  56. * @author Marien Fressinaud <dev@marienfressinaud.fr>
  57. * @link https://framagit.org/marienfressinaud/lib_opml
  58. * @license MIT
  59. */
  60. class LibOpml
  61. {
  62. /**
  63. * The list of valid head elements.
  64. */
  65. public const HEAD_ELEMENTS = [
  66. 'title', 'dateCreated', 'dateModified', 'ownerName', 'ownerEmail',
  67. 'ownerId', 'docs', 'expansionState', 'vertScrollState', 'windowTop',
  68. 'windowLeft', 'windowBottom', 'windowRight'
  69. ];
  70. /**
  71. * The list of numeric head elements.
  72. */
  73. public const NUMERIC_HEAD_ELEMENTS = [
  74. 'vertScrollState',
  75. 'windowTop',
  76. 'windowLeft',
  77. 'windowBottom',
  78. 'windowRight',
  79. ];
  80. /** @var boolean */
  81. private $strict = true;
  82. /** @var string */
  83. private $version = '2.0';
  84. /** @var string[] */
  85. private $namespaces = [];
  86. /**
  87. * @param bool $strict
  88. * Set to true (default) to check for violations of the specification,
  89. * false otherwise.
  90. */
  91. public function __construct($strict = true)
  92. {
  93. $this->strict = $strict;
  94. }
  95. /**
  96. * Parse a XML file and return the corresponding array.
  97. *
  98. * @param string $filename
  99. * The XML file to parse.
  100. *
  101. * @throws \marienfressinaud\LibOpml\Exception
  102. * Raised if the file cannot be read. See also exceptions raised by the
  103. * parseString method.
  104. *
  105. * @return array
  106. * An array reflecting the OPML (the structure is described above).
  107. */
  108. public function parseFile($filename)
  109. {
  110. $file_content = @file_get_contents($filename);
  111. if ($file_content === false) {
  112. throw new Exception("OPML file {$filename} cannot be found or read");
  113. }
  114. return $this->parseString($file_content);
  115. }
  116. /**
  117. * Parse a XML string and return the corresponding array.
  118. *
  119. * @param string $xml
  120. * The XML string to parse.
  121. *
  122. * @throws \marienfressinaud\LibOpml\Exception
  123. * Raised if the XML cannot be parsed, if version is missing or
  124. * invalid, if head is missing or contains invalid (or not parsable)
  125. * elements, or if body is missing, empty or contain non outline
  126. * elements. The exceptions (except XML parsing errors) are not raised
  127. * if strict is false. See also exceptions raised by the parseOutline
  128. * method.
  129. *
  130. * @return array
  131. * An array reflecting the OPML (the structure is described above).
  132. */
  133. public function parseString($xml)
  134. {
  135. $dom = new \DOMDocument();
  136. $dom->recover = true;
  137. $dom->encoding = 'UTF-8';
  138. try {
  139. $result = @$dom->loadXML($xml);
  140. } catch (\Exception | \Error $e) {
  141. $result = false;
  142. }
  143. if (!$result || !$dom->documentElement) {
  144. throw new Exception('OPML string is not valid XML');
  145. }
  146. $opml_element = $dom->documentElement;
  147. // Load the custom namespaces of the document
  148. $xpath = new \DOMXPath($dom);
  149. $this->namespaces = [];
  150. foreach ($xpath->query('//namespace::*') as $node) {
  151. if ($node->prefix === 'xml') {
  152. // This is the base namespace, we don't need to store it
  153. continue;
  154. }
  155. $this->namespaces[$node->prefix] = $node->namespaceURI;
  156. }
  157. // Get the version of the document
  158. $version = $opml_element->getAttribute('version');
  159. if (!$version) {
  160. $this->throwExceptionIfStrict('OPML version attribute is required');
  161. }
  162. $version = trim($version);
  163. if ($version === '1.1') {
  164. $version = '1.0';
  165. }
  166. if ($version !== '1.0' && $version !== '2.0') {
  167. $this->throwExceptionIfStrict('OPML supported versions are 1.0 and 2.0');
  168. }
  169. $this->version = $version;
  170. // Get head and body child elements
  171. $head_elements = $opml_element->getElementsByTagName('head');
  172. $child_head_elements = [];
  173. if (count($head_elements) === 1) {
  174. $child_head_elements = $head_elements[0]->childNodes;
  175. } else {
  176. $this->throwExceptionIfStrict('OPML must contain one and only one head element');
  177. }
  178. $body_elements = $opml_element->getElementsByTagName('body');
  179. $child_body_elements = [];
  180. if (count($body_elements) === 1) {
  181. $child_body_elements = $body_elements[0]->childNodes;
  182. } else {
  183. $this->throwExceptionIfStrict('OPML must contain one and only one body element');
  184. }
  185. $array = [
  186. 'version' => $this->version,
  187. 'namespaces' => $this->namespaces,
  188. 'head' => [],
  189. 'body' => [],
  190. ];
  191. // Load the child head elements in the head array
  192. foreach ($child_head_elements as $child_head_element) {
  193. if ($child_head_element->nodeType !== XML_ELEMENT_NODE) {
  194. continue;
  195. }
  196. $name = $child_head_element->nodeName;
  197. $value = $child_head_element->nodeValue;
  198. $namespaced = $child_head_element->namespaceURI !== null;
  199. if (!in_array($name, self::HEAD_ELEMENTS) && !$namespaced) {
  200. $this->throwExceptionIfStrict(
  201. "OPML head {$name} element is not part of the specification"
  202. );
  203. }
  204. if ($name === 'dateCreated' || $name === 'dateModified') {
  205. try {
  206. $value = $this->parseDate($value);
  207. } catch (\DomainException $e) {
  208. $this->throwExceptionIfStrict(
  209. "OPML head {$name} element must be a valid RFC822 or RFC1123 date"
  210. );
  211. }
  212. } elseif ($name === 'ownerEmail') {
  213. // Testing email validity is hard. PHP filter_var() function is
  214. // too strict compared to the RFC 822, so we can't use it.
  215. if (strpos($value, '@') === false) {
  216. $this->throwExceptionIfStrict(
  217. 'OPML head ownerEmail element must be an email address'
  218. );
  219. }
  220. } elseif ($name === 'ownerId' || $name === 'docs') {
  221. if (!$this->checkHttpAddress($value)) {
  222. $this->throwExceptionIfStrict(
  223. "OPML head {$name} element must be a HTTP address"
  224. );
  225. }
  226. } elseif ($name === 'expansionState') {
  227. $numbers = explode(',', $value);
  228. $value = array_map(function ($str_number) {
  229. if (is_numeric($str_number)) {
  230. return intval($str_number);
  231. } else {
  232. $this->throwExceptionIfStrict(
  233. 'OPML head expansionState element must be a list of numbers'
  234. );
  235. return $str_number;
  236. }
  237. }, $numbers);
  238. } elseif (in_array($name, self::NUMERIC_HEAD_ELEMENTS)) {
  239. if (is_numeric($value)) {
  240. $value = intval($value);
  241. } else {
  242. $this->throwExceptionIfStrict("OPML head {$name} element must be a number");
  243. }
  244. }
  245. $array['head'][$name] = $value;
  246. }
  247. // Load the child body elements in the body array
  248. foreach ($child_body_elements as $child_body_element) {
  249. if ($child_body_element->nodeType !== XML_ELEMENT_NODE) {
  250. continue;
  251. }
  252. if ($child_body_element->nodeName === 'outline') {
  253. $array['body'][] = $this->parseOutline($child_body_element);
  254. } else {
  255. $this->throwExceptionIfStrict(
  256. 'OPML body element can only contain outline elements'
  257. );
  258. }
  259. }
  260. if (empty($array['body'])) {
  261. $this->throwExceptionIfStrict(
  262. 'OPML body element must contain at least one outline element'
  263. );
  264. }
  265. return $array;
  266. }
  267. /**
  268. * Parse a XML element as an outline element and return the corresponding array.
  269. *
  270. * @param \DOMElement $outline_element
  271. * The element to parse.
  272. *
  273. * @throws \marienfressinaud\LibOpml\Exception
  274. * Raised if the outline contains non-outline elements, if it doesn't
  275. * contain a text attribute (or if empty), if a special attribute is
  276. * not parsable, or if type attribute requirements are not met. The
  277. * exceptions are not raised if strict is false. The exception about
  278. * missing text attribute is not raised if version is 1.0.
  279. *
  280. * @return array
  281. * An array reflecting the OPML outline (the structure is described above).
  282. */
  283. private function parseOutline($outline_element)
  284. {
  285. $outline = [];
  286. // Load the element attributes in the outline array
  287. foreach ($outline_element->attributes as $outline_attribute) {
  288. $name = $outline_attribute->nodeName;
  289. $value = $outline_attribute->nodeValue;
  290. if ($name === 'created') {
  291. try {
  292. $value = $this->parseDate($value);
  293. } catch (\DomainException $e) {
  294. $this->throwExceptionIfStrict(
  295. 'OPML outline created attribute must be a valid RFC822 or RFC1123 date'
  296. );
  297. }
  298. } elseif ($name === 'category') {
  299. $categories = explode(',', $value);
  300. $categories = array_map(function ($category) {
  301. return trim($category);
  302. }, $categories);
  303. $value = $categories;
  304. } elseif ($name === 'isComment' || $name === 'isBreakpoint') {
  305. if ($value === 'true' || $value === 'false') {
  306. $value = $value === 'true';
  307. } else {
  308. $this->throwExceptionIfStrict(
  309. "OPML outline {$name} attribute must be a boolean (true or false)"
  310. );
  311. }
  312. } elseif ($name === 'type') {
  313. // type attribute is case-insensitive
  314. $value = strtolower($value);
  315. }
  316. $outline[$name] = $value;
  317. }
  318. if (empty($outline['text']) && $this->version !== '1.0') {
  319. $this->throwExceptionIfStrict(
  320. 'OPML outline text attribute is required'
  321. );
  322. }
  323. // Perform additional check based on the type of the outline
  324. $type = $outline['type'] ?? '';
  325. if ($type === 'rss') {
  326. if (empty($outline['xmlUrl'])) {
  327. $this->throwExceptionIfStrict(
  328. 'OPML outline xmlUrl attribute is required when type is "rss"'
  329. );
  330. } elseif (!$this->checkHttpAddress($outline['xmlUrl'])) {
  331. $this->throwExceptionIfStrict(
  332. 'OPML outline xmlUrl attribute must be a HTTP address when type is "rss"'
  333. );
  334. }
  335. } elseif ($type === 'link' || $type === 'include') {
  336. if (empty($outline['url'])) {
  337. $this->throwExceptionIfStrict(
  338. "OPML outline url attribute is required when type is \"{$type}\""
  339. );
  340. } elseif (!$this->checkHttpAddress($outline['url'])) {
  341. $this->throwExceptionIfStrict(
  342. "OPML outline url attribute must be a HTTP address when type is \"{$type}\""
  343. );
  344. }
  345. }
  346. // Load the sub-outlines in a @outlines array
  347. foreach ($outline_element->childNodes as $child_outline_element) {
  348. if ($child_outline_element->nodeType !== XML_ELEMENT_NODE) {
  349. continue;
  350. }
  351. if ($child_outline_element->nodeName === 'outline') {
  352. $outline['@outlines'][] = $this->parseOutline($child_outline_element);
  353. } else {
  354. $this->throwExceptionIfStrict(
  355. 'OPML body element can only contain outline elements'
  356. );
  357. }
  358. }
  359. return $outline;
  360. }
  361. /**
  362. * Parse a value as a date.
  363. *
  364. * @param string $value
  365. *
  366. * @throws \DomainException
  367. * Raised if the value cannot be parsed.
  368. *
  369. * @return \DateTime
  370. */
  371. private function parseDate($value)
  372. {
  373. $formats = [
  374. \DateTimeInterface::RFC822,
  375. \DateTimeInterface::RFC1123,
  376. ];
  377. foreach ($formats as $format) {
  378. $date = date_create_from_format($format, $value);
  379. if ($date !== false) {
  380. return $date;
  381. }
  382. }
  383. throw new \DomainException('The argument cannot be parsed as a date');
  384. }
  385. /**
  386. * Render an OPML array as a string or a \DOMDocument.
  387. *
  388. * @param array $array
  389. * The array to render, it must follow the structure defined above.
  390. * @param bool $as_dom_document
  391. * Set to false (default) to return the array as a string, true to
  392. * return as a \DOMDocument.
  393. *
  394. * @throws \marienfressinaud\LibOpml\Exception
  395. * Raised if the `head` array contains unknown or invalid elements
  396. * (i.e. not of correct type), or if the `body` array is missing or
  397. * empty. The exceptions are not raised if strict is false. See also
  398. * exceptions raised by the renderOutline method.
  399. *
  400. * @return string|\DOMDocument
  401. * The XML string or DOM document corresponding to the given array.
  402. */
  403. public function render($array, $as_dom_document = false)
  404. {
  405. $dom = new \DOMDocument('1.0', 'UTF-8');
  406. $opml_element = new \DOMElement('opml');
  407. $dom->appendChild($opml_element);
  408. // Set the version attribute of the OPML document
  409. $version = $array['version'] ?? '2.0';
  410. if ($version === '1.1') {
  411. $version = '1.0';
  412. }
  413. if ($version !== '1.0' && $version !== '2.0') {
  414. $this->throwExceptionIfStrict('OPML supported versions are 1.0 and 2.0');
  415. }
  416. $this->version = $version;
  417. $opml_element->setAttribute('version', $this->version);
  418. // Declare the namespace on the opml element
  419. $this->namespaces = $array['namespaces'] ?? [];
  420. foreach ($this->namespaces as $prefix => $namespace) {
  421. $opml_element->setAttributeNS(
  422. 'http://www.w3.org/2000/xmlns/',
  423. "xmlns:{$prefix}",
  424. $namespace
  425. );
  426. }
  427. // Add the head element to the OPML document. $array['head'] is
  428. // optional but head tag will always exist in the final XML.
  429. $head_element = new \DOMElement('head');
  430. $opml_element->appendChild($head_element);
  431. if (isset($array['head'])) {
  432. foreach ($array['head'] as $name => $value) {
  433. $namespace = $this->getNamespace($name);
  434. if (!in_array($name, self::HEAD_ELEMENTS, true) && !$namespace) {
  435. $this->throwExceptionIfStrict(
  436. "OPML head {$name} element is not part of the specification"
  437. );
  438. }
  439. if ($name === 'dateCreated' || $name === 'dateModified') {
  440. if ($value instanceof \DateTimeInterface) {
  441. $value = $value->format(\DateTimeInterface::RFC1123);
  442. } else {
  443. $this->throwExceptionIfStrict(
  444. "OPML head {$name} element must be a DateTime"
  445. );
  446. }
  447. } elseif ($name === 'ownerEmail') {
  448. // Testing email validity is hard. PHP filter_var() function is
  449. // too strict compared to the RFC 822, so we can't use it.
  450. if (strpos($value, '@') === false) {
  451. $this->throwExceptionIfStrict(
  452. 'OPML head ownerEmail element must be an email address'
  453. );
  454. }
  455. } elseif ($name === 'ownerId' || $name === 'docs') {
  456. if (!$this->checkHttpAddress($value)) {
  457. $this->throwExceptionIfStrict(
  458. "OPML head {$name} element must be a HTTP address"
  459. );
  460. }
  461. } elseif ($name === 'expansionState') {
  462. if (is_array($value)) {
  463. foreach ($value as $number) {
  464. if (!is_int($number)) {
  465. $this->throwExceptionIfStrict(
  466. 'OPML head expansionState element must be an array of integers'
  467. );
  468. }
  469. }
  470. $value = implode(', ', $value);
  471. } else {
  472. $this->throwExceptionIfStrict(
  473. 'OPML head expansionState element must be an array of integers'
  474. );
  475. }
  476. } elseif (in_array($name, self::NUMERIC_HEAD_ELEMENTS)) {
  477. if (!is_int($value)) {
  478. $this->throwExceptionIfStrict(
  479. "OPML head {$name} element must be an integer"
  480. );
  481. }
  482. }
  483. $child_head_element = new \DOMElement($name, $value, $namespace);
  484. $head_element->appendChild($child_head_element);
  485. }
  486. }
  487. // Check body is set and contains at least one element
  488. if (!isset($array['body'])) {
  489. $this->throwExceptionIfStrict('OPML array must contain a body key');
  490. }
  491. $array_body = $array['body'] ?? [];
  492. if (count($array_body) <= 0) {
  493. $this->throwExceptionIfStrict(
  494. 'OPML body element must contain at least one outline array'
  495. );
  496. }
  497. // Create outline elements in the body element
  498. $body_element = new \DOMElement('body');
  499. $opml_element->appendChild($body_element);
  500. foreach ($array_body as $outline) {
  501. $this->renderOutline($body_element, $outline);
  502. }
  503. // And return the final result
  504. if ($as_dom_document) {
  505. return $dom;
  506. } else {
  507. $dom->formatOutput = true;
  508. return $dom->saveXML();
  509. }
  510. }
  511. /**
  512. * Transform an outline array to a \DOMElement and add it to a parent element.
  513. *
  514. * @param \DOMElement $parent_element
  515. * The DOM parent element of the current outline.
  516. * @param array $outline
  517. * The outline array to transform in a \DOMElement, it must follow the
  518. * structure defined above.
  519. *
  520. * @throws \marienfressinaud\LibOpml\Exception
  521. * Raised if the outline is not an array, if it doesn't contain a text
  522. * attribute (or if empty), if the `@outlines` key is not an array, if
  523. * a special attribute does not match its corresponding type, or if
  524. * `type` key requirements are not met. The exceptions (except errors
  525. * about outline or suboutlines not being arrays) are not raised if
  526. * strict is false. The exception about missing text attribute is not
  527. * raised if version is 1.0.
  528. */
  529. private function renderOutline($parent_element, $outline)
  530. {
  531. // Perform initial checks to verify the outline is correctly declared
  532. if (!is_array($outline)) {
  533. throw new Exception(
  534. 'OPML outline element must be defined as an array'
  535. );
  536. }
  537. if (empty($outline['text']) && $this->version !== '1.0') {
  538. $this->throwExceptionIfStrict(
  539. 'OPML outline text attribute is required'
  540. );
  541. }
  542. if (isset($outline['type'])) {
  543. $type = strtolower($outline['type']);
  544. if ($type === 'rss') {
  545. if (empty($outline['xmlUrl'])) {
  546. $this->throwExceptionIfStrict(
  547. 'OPML outline xmlUrl attribute is required when type is "rss"'
  548. );
  549. } elseif (!$this->checkHttpAddress($outline['xmlUrl'])) {
  550. $this->throwExceptionIfStrict(
  551. 'OPML outline xmlUrl attribute must be a HTTP address when type is "rss"'
  552. );
  553. }
  554. } elseif ($type === 'link' || $type === 'include') {
  555. if (empty($outline['url'])) {
  556. $this->throwExceptionIfStrict(
  557. "OPML outline url attribute is required when type is \"{$type}\""
  558. );
  559. } elseif (!$this->checkHttpAddress($outline['url'])) {
  560. $this->throwExceptionIfStrict(
  561. "OPML outline url attribute must be a HTTP address when type is \"{$type}\""
  562. );
  563. }
  564. }
  565. }
  566. // Create the outline element and add it to the parent
  567. $outline_element = new \DOMElement('outline');
  568. $parent_element->appendChild($outline_element);
  569. // Load the sub-outlines as child elements
  570. if (isset($outline['@outlines'])) {
  571. $outline_children = $outline['@outlines'];
  572. if (!is_array($outline_children)) {
  573. throw new Exception(
  574. 'OPML outline element must be defined as an array'
  575. );
  576. }
  577. foreach ($outline_children as $outline_child) {
  578. $this->renderOutline($outline_element, $outline_child);
  579. }
  580. // We don't want the sub-outlines to be loaded as attributes, so we
  581. // remove the key from the array.
  582. unset($outline['@outlines']);
  583. }
  584. // Load the other elements of the array as attributes
  585. foreach ($outline as $name => $value) {
  586. $namespace = $this->getNamespace($name);
  587. if ($name === 'created') {
  588. if ($value instanceof \DateTimeInterface) {
  589. $value = $value->format(\DateTimeInterface::RFC1123);
  590. } else {
  591. $this->throwExceptionIfStrict(
  592. 'OPML outline created attribute must be a DateTime'
  593. );
  594. }
  595. } elseif ($name === 'isComment' || $name === 'isBreakpoint') {
  596. if (is_bool($value)) {
  597. $value = $value ? 'true' : 'false';
  598. } else {
  599. $this->throwExceptionIfStrict(
  600. "OPML outline {$name} attribute must be a boolean"
  601. );
  602. }
  603. } elseif (is_array($value)) {
  604. $value = implode(', ', $value);
  605. }
  606. $outline_element->setAttributeNS($namespace, $name, $value);
  607. }
  608. }
  609. /**
  610. * Return wether a value is a valid HTTP address or not.
  611. *
  612. * HTTP address is not strictly defined by the OPML spec, so it is assumed:
  613. *
  614. * - it can be parsed by parse_url
  615. * - it has a host part
  616. * - scheme is http or https
  617. *
  618. * filter_var is not used because it would reject internationalized URLs
  619. * (i.e. with non ASCII chars). An alternative would be to punycode such
  620. * URLs, but it's more work to do it properly, and lib_opml needs to stay
  621. * simple.
  622. *
  623. * @param string $value
  624. *
  625. * @return boolean
  626. * Return true if the value is a valid HTTP address, false otherwise.
  627. */
  628. public function checkHttpAddress($value)
  629. {
  630. $value = trim($value);
  631. $parsed_url = parse_url($value);
  632. if (!$parsed_url) {
  633. return false;
  634. }
  635. if (
  636. !isset($parsed_url['scheme']) ||
  637. !isset($parsed_url['host'])
  638. ) {
  639. return false;
  640. }
  641. if (
  642. $parsed_url['scheme'] !== 'http' &&
  643. $parsed_url['scheme'] !== 'https'
  644. ) {
  645. return false;
  646. }
  647. return true;
  648. }
  649. /**
  650. * Return the namespace of a qualified name. An empty string is returned if
  651. * the name is not namespaced.
  652. *
  653. * @param string $qualified_name
  654. *
  655. * @throws \marienfressinaud\LibOpml\Exception
  656. * Raised if the namespace prefix isn't declared.
  657. *
  658. * @return string
  659. */
  660. private function getNamespace($qualified_name)
  661. {
  662. $split_name = explode(':', $qualified_name, 2);
  663. // count will always be 1 or 2.
  664. if (count($split_name) === 1) {
  665. // If 1, there's no prefix, thus no namespace
  666. return '';
  667. } else {
  668. // If 2, it means it has a namespace prefix, so we get the
  669. // namespace from the declared ones.
  670. $namespace_prefix = $split_name[0];
  671. if (!isset($this->namespaces[$namespace_prefix])) {
  672. throw new Exception(
  673. "OPML namespace {$namespace_prefix} is not declared"
  674. );
  675. }
  676. return $this->namespaces[$namespace_prefix];
  677. }
  678. }
  679. /**
  680. * Raise an exception only if strict is true.
  681. *
  682. * @param string $message
  683. *
  684. * @throws \marienfressinaud\LibOpml\Exception
  685. */
  686. private function throwExceptionIfStrict($message)
  687. {
  688. if ($this->strict) {
  689. throw new Exception($message);
  690. }
  691. }
  692. }