PageRenderTime 71ms CodeModel.GetById 42ms RepoModel.GetById 0ms app.codeStats 0ms

/inc/magpie/rss_parse.inc

https://github.com/chregu/fluxcms
PHP | 567 lines | 347 code | 69 blank | 151 comment | 83 complexity | 0d08bbf57a4299dd7c24e7586cba1597 MD5 | raw file
Possible License(s): GPL-2.0, BSD-3-Clause, Apache-2.0, LGPL-2.1
  1. <?php
  2. /*
  3. * Project: MagpieRSS: a simple RSS integration tool
  4. * File: rss_parse.inc - parse an RSS or Atom feed
  5. * return as a simple object.
  6. *
  7. * Handles RSS 0.9x, RSS 2.0, RSS 1.0, and Atom 0.3
  8. *
  9. * The lastest version of MagpieRSS can be obtained from:
  10. * http://magpierss.sourceforge.net
  11. *
  12. * For questions, help, comments, discussion, etc., please join the
  13. * Magpie mailing list:
  14. * magpierss-general@lists.sourceforge.net
  15. *
  16. * Author: Kellan Elliott-McCrea <kellan@protest.net>
  17. * Version: 0.6a
  18. * License: GPL
  19. *
  20. *
  21. * ABOUT MAGPIE's APPROACH TO PARSING:
  22. * - Magpie is based on expat, an XML parser, and therefore will only parse
  23. * valid XML files. This includes all properly constructed RSS or Atom.
  24. *
  25. * - Magpie is an inclusive parser. It will include any elements that
  26. * it can turn into a key value pair in the parsed feed object it returns.
  27. *
  28. * - Magpie supports namespaces, and will return any elements found in a
  29. * namespace in a sub-array, with the key point to that array being the
  30. * namespace prefix.
  31. * (e.g. if an item contains a <dc:date> element, then that date can
  32. * be accessed at $item['dc']['date']
  33. *
  34. * - Magpie supports nested elements by combining the names. If an item
  35. * includes XML like:
  36. * <author>
  37. * <name>Kellan</name>
  38. * </author>
  39. *
  40. * The name field is accessible at $item['author_name']
  41. *
  42. * - Magpie makes no attempt validate a feed beyond insuring that it
  43. * is valid XML.
  44. * RSS validators are readily available on the web at:
  45. * http://feeds.archive.org/validator/
  46. * http://www.ldodds.com/rss_validator/1.0/validator.html
  47. *
  48. *
  49. * EXAMPLE PARSED RSS ITEM:
  50. *
  51. * Magpie tries to parse RSS into easy to use PHP datastructures.
  52. *
  53. * For example, Magpie on encountering (a rather complex) RSS 1.0 item entry:
  54. *
  55. * <item rdf:about="http://protest.net/NorthEast/calendrome.cgi?span=event&#38;ID=210257">
  56. * <title>Weekly Peace Vigil</title>
  57. * <link>http://protest.net/NorthEast/calendrome.cgi?span=event&#38;ID=210257</link>
  58. * <description>Wear a white ribbon</description>
  59. * <dc:subject>Peace</dc:subject>
  60. * <ev:startdate>2002-06-01T11:00:00</ev:startdate>
  61. * <ev:location>Northampton, MA</ev:location>
  62. * <ev:type>Protest</ev:type>
  63. * </item>
  64. *
  65. * Would transform it into the following associative array, and push it
  66. * onto the array $rss-items
  67. *
  68. * array(
  69. * title => 'Weekly Peace Vigil',
  70. * link => 'http://protest.net/NorthEast/calendrome.cgi?span=event&#38;ID=210257',
  71. * description => 'Wear a white ribbon',
  72. * dc => array (
  73. * subject => 'Peace'
  74. * ),
  75. * ev => array (
  76. * startdate => '2002-06-01T11:00:00',
  77. * enddate => '2002-06-01T12:00:00',
  78. * type => 'Protest',
  79. * location => 'Northampton, MA'
  80. * )
  81. * )
  82. *
  83. *
  84. *
  85. * A FEW NOTES ON PARSING Atom FEEDS
  86. *
  87. * Atom support is considered alpha. Atom elements will be often be available
  88. * as their RSS equivalent, summary is available as description for example.
  89. *
  90. * Elements of mode=xml, as flattened into a single string, just as if they
  91. * had been wrapped in a CDATA container.
  92. *
  93. * See: http://laughingmeme.org/archives/001676.html
  94. *
  95. */
  96. define('RSS', 'RSS');
  97. define('ATOM', 'Atom');
  98. define('RDFNS', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
  99. class MagpieRSS {
  100. /*
  101. * Hybrid parser, and object. (probably a bad idea! :)
  102. *
  103. * Useage Example:
  104. *
  105. * $some_rss = "<?xml version="1.0"......
  106. *
  107. * $rss = new MagpieRSS( $some_rss );
  108. *
  109. * // print rss chanel title
  110. * echo $rss->channel['title'];
  111. *
  112. * // print the title of each item
  113. * foreach ($rss->items as $item ) {
  114. * echo $item[title];
  115. * }
  116. *
  117. * see: rss_fetch.inc for a simpler interface
  118. */
  119. public $parser;
  120. public $current_item = array(); // item currently being parsed
  121. public $items = array(); // collection of parsed items
  122. public $channel = array(); // hash of channel fields
  123. public $textinput = array();
  124. public $image = array();
  125. public $feed_type;
  126. public $feed_version;
  127. // parser publiciables
  128. public $stack = array(); // parser stack
  129. public $inchannel = false;
  130. public $initem = false;
  131. public $incontent = false; // if in Atom <content mode="xml"> field
  132. public $intextinput = false;
  133. public $inimage = false;
  134. public $current_field = '';
  135. public $current_namespace = false;
  136. public $namespaces = array( 'http://purl.org/rss/1.0/'=>'',
  137. RDFNS=>'rdf',
  138. 'http://purl.org/dc/elements/1.1/'=>'dc' ,
  139. 'http://purl.org/rss/1.0/modules/syndication/'=>'sy',
  140. 'http://webns.net/mvcb/'=>'admin' ,
  141. 'http://purl.org/rss/1.0/modules/content/'=>'content'
  142. );
  143. public $ERROR = "";
  144. public $_CONTENT_CONSTRUCTS = array('content', 'summary', 'info', 'title', 'tagline', 'copyright');
  145. /*======================================================================*\
  146. Function: MagpieRSS
  147. Purpose: Constructor, sets up XML parser,parses source,
  148. and populates object..
  149. Input: String containing the RSS to be parsed
  150. \*======================================================================*/
  151. function MagpieRSS ($source) {
  152. # if PHP xml isn't compiled in, die
  153. #
  154. if (!function_exists('xml_parser_create')) {
  155. $this->error( "Failed to load PHP's XML Extension. " .
  156. "http://www.php.net/manual/en/ref.xml.php",
  157. E_USER_ERROR );
  158. }
  159. $parser = @xml_parser_create_ns("UTF-8","@");
  160. xml_parser_set_option($parser,XML_OPTION_CASE_FOLDING,false);
  161. if (!is_resource($parser))
  162. {
  163. $this->error( "Failed to create an instance of PHP's XML parser. " .
  164. "http://www.php.net/manual/en/ref.xml.php",
  165. E_USER_ERROR );
  166. }
  167. $this->parser = $parser;
  168. # pass in parser, and a reference to this object
  169. # setup handlers
  170. #
  171. xml_set_object( $this->parser, $this );
  172. xml_set_element_handler($this->parser,
  173. 'feed_start_element', 'feed_end_element' );
  174. xml_set_character_data_handler( $this->parser, 'feed_cdata' );
  175. $status = xml_parse( $this->parser, $source );
  176. if (! $status ) {
  177. $errorcode = xml_get_error_code( $this->parser );
  178. if ( $errorcode != XML_ERROR_NONE ) {
  179. $xml_error = xml_error_string( $errorcode );
  180. $error_line = xml_get_current_line_number($this->parser);
  181. $error_col = xml_get_current_column_number($this->parser);
  182. $errormsg = "$xml_error at line $error_line, column $error_col";
  183. $this->error( $errormsg );
  184. }
  185. }
  186. xml_parser_free( $this->parser );
  187. $this->normalize();
  188. }
  189. function feed_start_element($p, $element, &$attrs) {
  190. $el = $element = strtolower($element);
  191. $attrs = array_change_key_case($attrs, CASE_LOWER);
  192. // check for a namespace, and split if found
  193. $ns = false;
  194. if ( strpos( $element, '@' ) ) {
  195. list($ns, $el) = split( '@', $element, 2);
  196. }
  197. if ( $ns and $ns != RDFNS) {
  198. $this->current_namespace = $this->namespaces[$ns];
  199. }
  200. # if feed type isn't set, then this is first element of feed
  201. # identify feed from root element
  202. #
  203. if (!isset($this->feed_type) ) {
  204. if ( $el == 'rdf' ) {
  205. $this->feed_type = RSS;
  206. $this->feed_version = '1.0';
  207. }
  208. elseif ( $el == 'rss' ) {
  209. $this->feed_type = RSS;
  210. $this->feed_version = $attrs['version'];
  211. }
  212. elseif ( $el == 'feed' ) {
  213. $this->feed_type = ATOM;
  214. $this->feed_version = $attrs['version'];
  215. $this->inchannel = true;
  216. }
  217. return;
  218. }
  219. if ( $el == 'channel' )
  220. {
  221. $this->inchannel = true;
  222. }
  223. elseif ($el == 'item' or $el == 'entry' )
  224. {
  225. $this->initem = true;
  226. if ( isset($attrs[RDFNS.'@about']) ) {
  227. $this->current_item['about'] = $attrs[RDFNS.'@about'];
  228. }
  229. }
  230. // if we're in the default namespace of an RSS feed,
  231. // record textinput or image fields
  232. elseif (
  233. $this->feed_type == RSS and
  234. $this->current_namespace == '' and
  235. $el == 'textinput' )
  236. {
  237. $this->intextinput = true;
  238. }
  239. elseif (
  240. $this->feed_type == RSS and
  241. $this->current_namespace == '' and
  242. $el == 'image' )
  243. {
  244. $this->inimage = true;
  245. }
  246. # handle atom content constructs
  247. elseif ( $this->feed_type == ATOM and in_array($el, $this->_CONTENT_CONSTRUCTS) )
  248. {
  249. // avoid clashing w/ RSS mod_content
  250. if ($el == 'content' ) {
  251. $el = 'atom_content';
  252. }
  253. $this->incontent = $el;
  254. }
  255. // if inside an Atom content construct (e.g. content or summary) field treat tags as text
  256. elseif ($this->feed_type == ATOM and $this->incontent )
  257. {
  258. // if tags are inlined, then flatten
  259. $attrs_str = join(' ',
  260. array_map('map_attrs',
  261. array_keys($attrs),
  262. array_values($attrs) ) );
  263. $this->append_content( "<$element $attrs_str>" );
  264. array_unshift( $this->stack, $el );
  265. }
  266. // Atom support many links per containging element.
  267. // Magpie treats link elements of type rel='alternate'
  268. // as being equivalent to RSS's simple link element.
  269. //
  270. elseif ($this->feed_type == ATOM and $el == 'link' )
  271. {
  272. if ( isset($attrs['rel']) and $attrs['rel'] == 'alternate' )
  273. {
  274. $link_el = 'link';
  275. }
  276. else {
  277. $link_el = 'link_' . $attrs['rel'];
  278. }
  279. $this->append($link_el, $attrs['href']);
  280. }
  281. // set stack[0] to current element
  282. else {
  283. array_unshift($this->stack, $el);
  284. }
  285. }
  286. function feed_cdata ($p, $text) {
  287. if ($this->feed_type == ATOM and $this->incontent)
  288. {
  289. $this->append_content( $text );
  290. }
  291. else {
  292. $current_el = join('_', array_reverse($this->stack));
  293. $this->append($current_el, $text);
  294. }
  295. }
  296. function feed_end_element ($p, $el) {
  297. $el = strtolower($el);
  298. if ( strpos( $el, '@' ) ) {
  299. list($ns, $el) = split( '@', $el, 2);
  300. }
  301. if ( $ns ) {
  302. $ns = $this->namespaces[$ns];
  303. }
  304. if ( $el == 'item' or $el == 'entry' )
  305. {
  306. $this->items[] = $this->current_item;
  307. $this->current_item = array();
  308. $this->initem = false;
  309. }
  310. elseif ($this->feed_type == RSS and $this->current_namespace == '' and $el == 'textinput' )
  311. {
  312. $this->intextinput = false;
  313. }
  314. elseif ($this->feed_type == RSS and $this->current_namespace == '' and $el == 'image' )
  315. {
  316. $this->inimage = false;
  317. }
  318. elseif ($this->feed_type == ATOM and in_array($el, $this->_CONTENT_CONSTRUCTS) )
  319. {
  320. $this->incontent = false;
  321. }
  322. elseif ($el == 'channel' or $el == 'feed' )
  323. {
  324. $this->inchannel = false;
  325. }
  326. elseif ($this->feed_type == ATOM and $this->incontent ) {
  327. // balance tags properly
  328. // note: i don't think this is actually neccessary
  329. if ( $this->stack[0] == $el )
  330. {
  331. $this->append_content("</$el>");
  332. }
  333. else {
  334. $this->append_content("<$el />");
  335. }
  336. array_shift( $this->stack );
  337. }
  338. else {
  339. array_shift( $this->stack );
  340. }
  341. $this->current_namespace = false;
  342. }
  343. function concat (&$str1, $str2="") {
  344. if (!isset($str1) ) {
  345. $str1="";
  346. }
  347. $str1 .= $str2;
  348. }
  349. function append_content($text) {
  350. if ( $this->initem ) {
  351. $this->concat( $this->current_item[ $this->incontent ], $text );
  352. }
  353. elseif ( $this->inchannel ) {
  354. $this->concat( $this->channel[ $this->incontent ], $text );
  355. }
  356. }
  357. // smart append - field and namespace aware
  358. function append($el, $text) {
  359. if (!$el) {
  360. return;
  361. }
  362. if ( $this->current_namespace )
  363. {
  364. if ( $this->initem ) {
  365. $this->concat(
  366. $this->current_item[ $this->current_namespace ][ $el ], $text);
  367. }
  368. elseif ($this->inchannel) {
  369. $this->concat(
  370. $this->channel[ $this->current_namespace][ $el ], $text );
  371. }
  372. elseif ($this->intextinput) {
  373. $this->concat(
  374. $this->textinput[ $this->current_namespace][ $el ], $text );
  375. }
  376. elseif ($this->inimage) {
  377. $this->concat(
  378. $this->image[ $this->current_namespace ][ $el ], $text );
  379. }
  380. }
  381. else {
  382. if ( $this->initem ) {
  383. $this->concat(
  384. $this->current_item[ $el ], $text);
  385. }
  386. elseif ($this->intextinput) {
  387. $this->concat(
  388. $this->textinput[ $el ], $text );
  389. }
  390. elseif ($this->inimage) {
  391. $this->concat(
  392. $this->image[ $el ], $text );
  393. }
  394. elseif ($this->inchannel) {
  395. $this->concat(
  396. $this->channel[ $el ], $text );
  397. }
  398. }
  399. }
  400. function normalize () {
  401. // if atom populate rss fields
  402. if ( $this->is_atom() ) {
  403. $this->channel['descripton'] = $this->channel['tagline'];
  404. for ( $i = 0; $i < count($this->items); $i++) {
  405. $item = $this->items[$i];
  406. if ( isset($item['summary']) )
  407. $item['description'] = $item['summary'];
  408. if ( isset($item['atom_content']))
  409. $item['content']['encoded'] = $item['atom_content'];
  410. $this->items[$i] = $item;
  411. }
  412. }
  413. elseif ( $this->is_rss() ) {
  414. $this->channel['tagline'] = $this->channel['description'];
  415. for ( $i = 0; $i < count($this->items); $i++) {
  416. $item = $this->items[$i];
  417. if ( isset($item['description']))
  418. $item['summary'] = $item['description'];
  419. if ( isset($item['content']['encoded'] ) )
  420. $item['atom_content'] = $item['content']['encoded'];
  421. $this->items[$i] = $item;
  422. }
  423. }
  424. }
  425. function error ($errormsg, $lvl=E_USER_WARNING) {
  426. // append PHP's error message if track_errors enabled
  427. if ( $php_errormsg ) {
  428. $errormsg .= " ($php_errormsg)";
  429. }
  430. $this->ERROR = $errormsg;
  431. if ( MAGPIE_DEBUG ) {
  432. trigger_error( $errormsg, $lvl);
  433. }
  434. else {
  435. error_log( $errormsg, 0);
  436. }
  437. }
  438. function is_rss () {
  439. if ( $this->feed_type == RSS ) {
  440. return $this->feed_version;
  441. }
  442. else {
  443. return false;
  444. }
  445. }
  446. function is_atom() {
  447. if ( $this->feed_type == ATOM ) {
  448. return $this->feed_version;
  449. }
  450. else {
  451. return false;
  452. }
  453. }
  454. /*======================================================================*\
  455. EVERYTHING BELOW HERE IS FOR DEBUGGING PURPOSES
  456. \*======================================================================*/
  457. function show_list () {
  458. echo "<ol>\n";
  459. foreach ($this->items as $item) {
  460. echo "<li>", $this->show_item( $item );
  461. }
  462. echo "</ol>";
  463. }
  464. function show_channel () {
  465. echo "channel:<br>";
  466. echo "<ul>";
  467. while ( list($key, $value) = each( $this->channel ) ) {
  468. echo "<li> $key: $value";
  469. }
  470. echo "</ul>";
  471. }
  472. function show_item ($item) {
  473. echo "item: $item[title]";
  474. echo "<ul>";
  475. while ( list($key, $value) = each($item) ) {
  476. if ( is_array($value) ) {
  477. echo "<br><b>$key</b>";
  478. echo "<ul>";
  479. while ( list( $ns_key, $ns_value) = each( $value ) ) {
  480. echo "<li>$ns_key: $ns_value";
  481. }
  482. echo "</ul>";
  483. }
  484. else {
  485. echo "<li> $key: $value";
  486. }
  487. }
  488. echo "</ul>";
  489. }
  490. /*======================================================================*\
  491. END DEBUGGING FUNCTIONS
  492. \*======================================================================*/
  493. } # end class RSS
  494. function map_attrs($k, $v) {
  495. return "$k=\"$v\"";
  496. }
  497. ?>