<?php

class NP_XMLSupport extends NucleusPlugin {

   /* ==========================================================================================
	* XML Support Library 0.14
	* for NP_RSSAtom, NP_RSSAtomAggregator and NP_XBEL
	*
	* Copyright 2004 by Niels Leenheer
	* ==========================================================================================
	* This program is free software and open source software; you can redistribute
	* it and/or modify it under the terms of the GNU General Public License as
	* published by the Free Software Foundation; either version 2 of the License,
	* or (at your option) any later version.
	*
	* This program is distributed in the hope that it will be useful, but WITHOUT
	* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
	* FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
	* more details.
	*
	* You should have received a copy of the GNU General Public License along
	* with this program; if not, write to the Free Software Foundation, Inc.,
	* 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA  or visit
	* http://www.gnu.org/licenses/gpl.html
	* ==========================================================================================
	*
	* 0.14   - Again rewritten large parts of the charset conversion
	* 0.13   - Replaced code for charset conversion with faster optimized version
	* 0.12   - Added proper support for UTF-8 encoded RSS and Atom feeds
	*        - Added charset conversion for more exotic feeds
	* 0.11   - Content parsing now takes the parent node into account,
	*          solving problems with parsing the /. feed
	* 0.10   - Wrapped a plugin around these classes, with its own
	*          general settings that apply to all plugins
	* 0.9.6  - Fixed a typo
	* 0.9.5  - Initial release
	*        - Imported RSS/Atoms cache from NP_RSSAtom
	*        - Imported RSS/Atoms parser from NP_RSSAtom
	*        - Imported XBEL parser from NP_XBEL
	*        - Imported OPML parser from NP_RSSAtomAggregator
	*        - Added ETag compatible URL reader
	*        - Added a random element to cache TTL
	*        - Misc bugfixes
	*/


	function getName() {
		return 'XML Parser Support Library';
	}

	function getAuthor()  {
		return 'Niels Leenheer';
	}

	function getURL() {
		return 'http://www.rakaz.nl/nucleus/extra/plugins';
	}

	function getVersion() {
		return '0.14';
	}

	function getDescription() {
		return 'This plugin provides methods for parsing RSS/Atom, OPML and XBEL
				files. Without this plugin other plugins like RSS/Atom Reader, 
				RSS/Atom Aggregator and XBEL Reader will not function.';
	}
	
	function supportsFeature($feature) {
    	switch($feature) {
	        case 'SqlTablePrefix':
	        	return 1;
	        default:
	    		return 0;
		}
	}

	var $cacheFolder;
	var $cacheTTL;
	var $entity;

	function init() {
		global $DIR_MEDIA;
		if (!is_dir($DIR_MEDIA)) die('System is not configured properly');
		$this->cacheFolder = $DIR_MEDIA.$this->getOption('cacheFolder');
		$this->cacheTTL = (int) $this->getOption('cacheTTL');
		
		// Include charset conversion library...
		include ($this->getDirectory().'/utf8.php');
	}	
	
	function install() {
    	$this->createOption('cacheFolder','Cache directory','text','.cache');
    	$this->createOption('cacheTTL','Cache maximum Age','select','30','15 minutes|15|30 minutes|30|60 minutes|60');
	}
	
	function createRSSAtomParser($cached = true) {
		if ($cached) 
		{
			$parser = new RSSAtomParserCache();
			$parser->setDirectory($this->getDirectory());
			$parser->setFolder($this->cacheFolder);
			$parser->setTTL($this->cacheTTL);
		}
		else
		{
			$parser = new RSSAtomParser();
			$parser->setDirectory($this->getDirectory());
		}

		return $parser;
	}

	function createOPMLParser() {
		$parser = new OPMLParser();
		$parser->setDirectory($this->getDirectory());
		return $parser;
	}

	function createXBELParser() {
		$parser = new XBELParser();
		$parser->setDirectory($this->getDirectory());
		return $parser;
	}
}



class XMLParserBase {
   /* ----------------------
	* Base class that provides error and debug reporting facilities
	*/
	
	var $debug = false;
	var $error = false;

	var $directory;
	
	var $encoding = 'utf-8';

	function XMLParserBase() {
	}	
	
	function setDirectory($directory) {
		$this->directory = $directory;
	}
	
	function startTimer() {
		$this->start_timer = $this->getTime();
	}
	
	function stopTimer() {
		$current = $this->getTime();
		return $current - $this->start_timer;
	}
	
	function getTime() {
   		list($usec, $sec) = explode(" ", microtime());
   		return ((float)$usec + (float)$sec);
	} 		
	
	function _iso8601 ($date_str) {

		/* Taken from:
		 * MagpieRSS: a simple RSS integration tool
		 * Kellan Elliott-McCrea <kellan@protest.net>
		 */
	
		$pat = "/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(:(\d{2}))?(?:([-+])(\d{2}):?(\d{2})|(Z))?/";
		
		if (preg_match( $pat, $date_str, $match)) 
		{
			list( $year, $month, $day, $hours, $minutes, $seconds) = 
				array( $match[1], $match[2], $match[3], $match[4], $match[5], $match[6]);
			
			// Calc epoch for current date assuming GMT
			$epoch = gmmktime( $hours, $minutes, $seconds, $month, $day, $year);
			
			$offset = 0;
			if ( $match[10] == 'Z' ) {
				// Zulu time, aka GMT
			}
			else 
			{
				list( $tz_mod, $tz_hour, $tz_min ) =
					array( $match[8], $match[9], $match[10]);
				
				// Zero out the variables
				if ( ! $tz_hour ) { $tz_hour = 0; }
				if ( ! $tz_min ) { $tz_min = 0; }
			
				$offset_secs = (($tz_hour*60)+$tz_min)*60;
				
				// Is timezone ahead of GMT?  then subtract offset
				if ( $tz_mod == '+' ) {
					$offset_secs = $offset_secs * -1;
				}
				
				$offset = $offset_secs;	
			}

			$epoch = $epoch + $offset;
			return $epoch;
		}
		else {
			return -1;
		}
	}

	function setError ($flag) {
		$this->error = $flag;
	}
	
	function setDebug ($flag) {
		$this->debug = $flag;
	}

	function _debug ($string) {
		if ($this->debug) 
			print "DEBUG: " . $string . "<br />";
	}

	function _error ($string) {
		if ($this->error) 
			print "ERROR: " . $string . "<br />";
	}
}



class XMLParserRetrieve extends XMLParserBase {
   /* ----------------------
	* Class that provides URL retrieval, including ETag support
	*/

	var $url;
	var $etag_in;
	var $etag_out;
	var $contents;
	var $response;
	var $charset;
	
	function XMLParserRetrieve($url = '') {
		$this->etag_out = '';
		$this->response = '';
		$this->url = $url;
	}
	
	function setUrl($url) {
		$this->etag_out = '';
		$this->url = $url;
	}
	
	function getContents() {

		$this->_fetch($this->url);
		return $this->contents;
	}
	
	function setEtag($etag) {
		$this->etag_in = $etag;
	}

	function getEtag() {
		return $this->etag_out;
	}
	
	function getResponse() {
		return $this->response;
	}
	
	function getCharset() {
		return $this->charset;
	}

	function _fetch($url) {

		if (function_exists('curl_init'))
		{
			if (isset($this->etag_in))
			{
				$header = array (
					'If-None-Match: "'.$this->etag_in.'"'
				); 
			}
			else
			{
				$header = array();
			}
		
			// Set options
			$ch = curl_init();
			curl_setopt($ch, CURLOPT_URL, $url);
			curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
			curl_setopt($ch, CURLOPT_HEADER, 1);
			curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
			curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1);
			curl_setopt($ch, CURLOPT_TIMEOUT, 10);
			
			// Retrieve response
			$this->contents = curl_exec($ch);
			$info 		    = curl_getinfo($ch);
			$this->response = $info['http_code'];
			
			// Split into headers and contents
			$headers = substr($this->contents, 0, $info['header_size']);
			$this->contents = substr($this->contents, $info['header_size']);
			
			// Check if ETtag is present
			if (preg_match ('/ETag:[^"]+"([^\n]+)"/is', $headers, $regs))
				$this->etag_out = trim($regs[1]);
			
			if (preg_match ("/charset=([^\n]+)/is", $headers, $regs))
				$this->charset = trim($regs[1]);
			
//			echo $headers;
			
			curl_close($ch);
		}
		else
		{
			if ($this->contents = @file_get_contents($url))
				$this->response = '200';
			else
				$this->response = '404';
			
			$this->etag_out = '';
		}
	}
}



class OPMLParser extends XMLParserBase {
   /* ----------------------
	* Parse OPML files and return the contents
	*/

	var $tagname;
	var $content;
	
	var $converter;

	function OPMLParser() {
		$this->tagname = '';
		$this->content = array();
		$this->history = array( & $this->content);
	}
	
	function readFile ($file) {
		if ($raw = @file_get_contents ($file))
		{		
			$this->converter = new CharsetEntity();
	
			// Detect encoding...
			if ($detected = $this->converter->detectDeclaration($raw))
				$encoding = $detected;
			
			// If encoding is still empty...
			if ($encoding == '')
				$encoding = 'iso-8859-1';
	
			// Convert to UTF-8
			$raw = $this->converter->convert($raw, $encoding);
			$encoding = 'utf-8';

			$this->xml_parser = @xml_parser_create('UTF-8');
			@xml_set_object($this->xml_parser, $this);
			@xml_parser_set_option($this->xml_parser, XML_OPTION_CASE_FOLDING, true);
			@xml_parser_set_option($this->xml_parser, XML_OPTION_TARGET_ENCODING, 'UTF-8');
			@xml_set_element_handler($this->xml_parser, "_startElement", "_endElement");
			@xml_set_character_data_handler($this->xml_parser, "_characterData");

			if (!@xml_parse($this->xml_parser, $raw, true)) {
			
				$this->_error(sprintf("XML malformed: %s at line %d",
					xml_error_string(xml_get_error_code($this->xml_parser)),
					xml_get_current_line_number($this->xml_parser)));
				
				return;
			}

			@xml_parser_free($this->xml_parser);
			
			return ($this->content);
		}
		else
		{
			$this->_error(sprintf ("OPML file %s not found!", $filename));
		}
	}
	
	function _startElement($parser, $name, $attrs) {
		$this->tagname = $name;
	
		switch($name) 
		{
			case 'OUTLINE':
				if (isset($attrs['TEXT']) && $attrs['TEXT'] != '' &&
					isset($attrs['XMLURL']) && $attrs['XMLURL'] != '')
				{
					$current = array();
					$current['owner'] = $attrs['TEXT'];
					$current['feed'] = $attrs['XMLURL'];
			
					$this->content['feeds'][] = & $current;
				}
				break;

			default:
				break;
		}
	}

	function _endElement($parser, $name) {
	}
	
	function _characterData($parser, $data) {
		switch($this->tagname) 
		{
			case 'TITLE':
				if (isset($this->content['title']))
					$this->content['title'] .= $data;								
				else
					$this->content['title'] = $data;								
				break;
	
			default:
				break;
		}
	}		
}



class XBELParser extends XMLParserBase {
   /* ----------------------
	* Parse XBEL files and return the contents
	*/
	
	var $tagname;
	var $content;
	var $history;

	function XBELParser() {
		$this->tagname = '';
		$this->content = array();
		$this->history = array( & $this->content);
	}
	
	function readFile ($file) {
		if ($raw = @file_get_contents ($file))
		{		
			$converter = new CharsetEntity();
	
			// Detect encoding...
			if ($detected = $converter->detectDeclaration($raw))
				$encoding = $detected;
			
			// If encoding is still empty...
			if ($encoding == '')
				$encoding = 'iso-8859-1';
	
			// Convert to UTF-8
			$raw = $converter->convert($raw, $encoding);
			$encoding = 'utf-8';

			$this->xml_parser = @xml_parser_create('UTF-8');
			@xml_set_object($this->xml_parser, $this);
			@xml_parser_set_option($this->xml_parser, XML_OPTION_CASE_FOLDING, true);
			@xml_parser_set_option($this->xml_parser, XML_OPTION_TARGET_ENCODING, 'UTF-8');
			@xml_set_element_handler($this->xml_parser, "_startElement", "_endElement");
			@xml_set_character_data_handler($this->xml_parser, "_characterData");
	
			if (!@xml_parse($this->xml_parser, $raw, true)) {
				$this->_error(sprintf("XML malformed: %s at line %d",
					xml_error_string(xml_get_error_code($this->xml_parser)),
					xml_get_current_line_number($this->xml_parser)));
				
				return;
			}

			@xml_parser_free($this->xml_parser);

			return ($this->content);
		}
		else
		{
			$this->_debug(sprintf ("XBEL file %s not found!", $filename));
			$this->_error(sprintf ("XBEL file %s not found!", $filename));
		}
	}
	
	function _startElement($parser, $name, $attrs) {
		$this->tagname = $name;
	
		switch($name) 
		{
			case 'FOLDER':
				$current = & $this->history[count($this->history) - 1];
				$level = array('type' => 'folder');
				$current['children'][] = & $level;
				$this->history[] = & $level;
				break;
				
			case 'BOOKMARK':
				$current = & $this->history[count($this->history) - 1];
				$level = array('type' => 'bookmark', 'url' => $attrs['HREF']);
				$current['children'][] = & $level;
				$this->history[] = & $level;
				break;
				
			case 'SEPARATOR':
				$current = & $this->history[count($this->history) - 1];
				$level = array('type' => 'separator');
				$current['children'][] = & $level;
				$this->history[] = & $level;
				break;
	
			case 'METADATA':
				if (isset($attrs['OWNER']) && $attrs['OWNER'] == 'Mozilla' &&
					isset($attrs['FEEDURL']) && $attrs['FEEDURL'] != '')
				{
					$current = & $this->history[count($this->history) - 1];
					$current['feed'] = $attrs['FEEDURL'];
					$current['type'] = 'livemark';
				}
				break;

			default:
				break;
		}
	}

	function _endElement($parser, $name) {
		switch($name) 
		{
			case 'FOLDER':
			case 'BOOKMARK':
			case 'SEPARATOR':
				array_pop($this->history);
				break;
	
			default:
				break;
		}
	}
	
	function _characterData($parser, $data) {
		switch($this->tagname) 
		{
			case 'TITLE':
				$current = & $this->history[count($this->history) - 1];
				if (isset($current['title']))
					$current['title'] .= $data;								
				else
					$current['title'] = $data;								
				break;
	
			case 'DESC':
				$current = & $this->history[count($this->history) - 1];
				if (isset($current['description']))
					$current['description'] .= $data;								
				else
					$current['description'] = $data;								
				break;
	
			default:
				break;
		}
	}		
}



class RSSAtomParser extends XMLParserBase {
   /* ----------------------
	* Parse RSS or Atom files and return the contents
	*/

	var $feed_tagname;
	var $feed_history;
	var $data_content;
	var $data_pointer;
	var $counter;
	
	
	function RSSAtomParser () {
		$this->feed_tagname = '';
		$this->feed_history = array();
		
		$this->data_content = array();
		$this->data_pointer = array( & $this->data_content);
	}
	
	function readURL ($url) {

		if ($this->debug) $this->startTimer();

		$reader = new XMLParserRetrieve();
		$reader->setUrl ($url);
		$raw = $reader->getContents();

		if ($this->debug) 
		{
			$time = $this->stopTimer();
			$this->_debug("Retrieve data time: ".$time);
		}

		$this->_debug(sprintf ("Retrieving feed from URL %s", $url));
		$this->_debug(sprintf ("Response %d", $reader->getResponse()));

		if ($reader->getResponse() == '200')
		{
			$etag 	  = $reader->getEtag();
			$encoding = $reader->getCharset();

			$this->_debug(sprintf ("Server character set: %s", $encoding));
			
			return ($this->_parse($raw, $etag, $url, $encoding));
		}
		else
		{
			$this->_error(sprintf ("Could not retrieve feed from '%s'", $url));
			$this->_debug("Could not retrieve feed");
			
			return false;
		}
	}
	
	function _parse($raw, $etag, $url, $encoding) {

		$converter = new CharsetEntity();

		// Detect encoding...
		if ($detected = $converter->detectDeclaration($raw))
		{
			$encoding = $detected;
			$this->_debug(sprintf ("XML signature character set: %s", $encoding));
		}
		else
		{
			$this->_debug("XML signature not found");
		}
		
		// If encoding is still empty...
		if ($encoding == '')
		{
			$encoding = 'iso-8859-1';
			$this->_debug(sprintf ("Using default character set: %s", $encoding));
		}

		// Convert to UTF-8
		$this->_debug("Charset conversion (".$encoding." to utf-8)");

		if ($this->debug) $this->startTimer();

		$raw = $converter->convert($raw, $encoding);
		$encoding = 'utf-8';

		if ($this->debug) 
		{
			$time = $this->stopTimer();
			$this->_debug("Charset conversion time: ".$time);
		}
		
		// Get BiDi from characterset
		$this->bidi = $converter->bidi($encoding);

		$this->data_content['time'] = time();
		$this->data_content['etag'] = $etag;
		$this->data_content['url'] = $url;
		$this->data_content['bidi'] = $this->bidi;
		$this->data_content['encoding'] = $encoding;
		
		if ($this->debug) $this->startTimer();

		$this->xml_parser = @xml_parser_create_ns('UTF-8');
		@xml_set_object($this->xml_parser, $this);
		@xml_parser_set_option($this->xml_parser, XML_OPTION_CASE_FOLDING, true);
		@xml_parser_set_option($this->xml_parser, XML_OPTION_TARGET_ENCODING, 'UTF-8');
		@xml_set_element_handler($this->xml_parser, "_startElement", "_endElement");
		@xml_set_character_data_handler($this->xml_parser, "_characterData");

		if (!@xml_parse($this->xml_parser, $raw, true)) {
		
			$this->_error(sprintf("XML malformed: %s at line %d",
				xml_error_string(xml_get_error_code($this->xml_parser)),
				xml_get_current_line_number($this->xml_parser)));

			$this->_debug(sprintf("XML malformed: %s at line %d",
				xml_error_string(xml_get_error_code($this->xml_parser)),
				xml_get_current_line_number($this->xml_parser)));
			
			return;
		}

		@xml_parser_free($this->xml_parser);	

		if ($this->debug) 
		{
			$time = $this->stopTimer();
			$this->_debug("Parsing time: ".$time);
		}

		return ($this->data_content);
	}
	
	function _startElement($parser, $name, $attrs) {
	
		if (preg_match("%^(.+):([A-Z]+)$%", $name, $regs))
		{
			$namespace = $regs[1];
			$element = $regs[2];
		}
		else
		{
			$namespace = '';
			$element = $name;
		}
		
		/* Feed detection */
		if (!isset($this->detected_feed))
		{
			if ($namespace == '' && $element == 'RSS')
			{
				if (isset($attrs['VERSION']) && (int) $attrs['VERSION'][0] > 0)
					$this->detected_feed = 'RSS/2.0';
				else
					$this->detected_feed = 'RSS/0.9';

				$this->detected_namespace = $namespace;
			}
			
			if ($namespace == 'HTTP://PURL.ORG/RSS/1.0/')
			{
				$this->detected_feed = 'RSS/1.0';
				$this->detected_namespace = $namespace;
			}

			if ($namespace == 'HTTP://PURL.ORG/ATOM/NS#')
			{
				$this->detected_feed = 'Atom';
				$this->detected_namespace = $namespace;
			}
			
			if (isset($this->detected_feed))
			{
				$this->data_content['feed'] = $this->detected_feed;
			}
		}
		
		/* Handle element */
		if (isset($this->detected_namespace) && $namespace == $this->detected_namespace)
		{
			switch($element) 
			{
				case 'ENTRY':
				case 'ITEM':
					$insert  = array('bidi' => $this->bidi);
					$current = & $this->data_pointer[count($this->data_pointer) - 1];
					$current['items'][] = & $insert;
					$this->data_pointer[] = & $insert;
					break;
				
				case 'LINK':
					if ($this->detected_feed == 'Atom')
					{
						$data_current = & $this->data_pointer[count($this->data_pointer) - 1];

						if (strtolower($attrs['REL']) == 'alternate' &&
							$attrs['HREF'] != '')
						{
							switch (strtolower($attrs['TYPE'])) 
							{
								case 'text/html':
								case 'application/xhtml+xml':
									$data_current['href'] = $attrs['HREF'];
									break;
								
								default:
									break;
							}
						}
					}
					break;
				
				default:
					break;				
			}
		}

		$this->feed_history[] = array('type' => $element, 'ns' => $namespace, 'id' => $this->counter);
	}
	
	function _endElement($parser, $name) {

		if (preg_match("%^(.+):([A-Z]+)$%", $name, $regs))
		{
			$namespace = $regs[1];
			$element = $regs[2];
		}
		else
		{
			$namespace = '';
			$element = $name;
		}

		/* Handle element */
		if (isset($this->detected_namespace) && $namespace == $this->detected_namespace)
		{

			switch($element) 
			{
				case 'ENTRY':
				case 'ITEM':
					array_pop($this->data_pointer);
					break;
				
				default:
					break;				
			}
		}

		array_pop($this->feed_history);
	}
	
	function _characterData($parser, $data) {
	
		$feed_current = & $this->feed_history[count($this->feed_history) - 1];
		$data_current = & $this->data_pointer[count($this->data_pointer) - 1];
		
		/* Parse attributes that belong to the correct name space */
		if (isset($this->detected_namespace) && $feed_current['ns'] == $this->detected_namespace)
		{
			// Get last parent in the same namespace
			if (count($this->feed_history) > 1)
			{
				$loop = count($this->feed_history) - 2;
			
				while ($loop >= 0)
				{
					if ($this->feed_history[$loop]['ns'] == $feed_current['ns'])
					{
						$parent = $this->feed_history[$loop]['type'];
						break;
					}
				
					$loop--;
				}
			}
			else
			{
				$parent = '';	
			}
			
			if ($this->detected_feed == 'RSS/0.9' &&
			   ($parent == 'ITEM' || $parent == 'CHANNEL'))
			{
				switch($feed_current['type'])
				{
					case 'TITLE':
						if (isset($data_current['title']))
							$data_current['title'] .= $data;
						else
							$data_current['title'] = $data;
						break;
					
					case 'LINK':
						if (isset($data_current['href']))
							$data_current['href'] .= $data;
						else
							$data_current['href'] = $data;
						break;

					default:
						break;
				}
			}
			
			if ($this->detected_feed == 'RSS/1.0' &&
			   ($parent == 'ITEM' || $parent == 'CHANNEL'))
			{
				switch($feed_current['type'])
				{
					case 'TITLE':
						if (isset($data_current['title']))
							$data_current['title'] .= $data;
						else
							$data_current['title'] = $data;
						break;
					
					case 'LINK':
						if (isset($data_current['href']))
							$data_current['href'] .= $data;
						else
							$data_current['href'] = $data;
						break;

					default:
						break;
				}
			}
			
			if ($this->detected_feed == 'RSS/2.0' &&
			   ($parent == 'ITEM' || $parent == 'CHANNEL'))
			{
				switch($feed_current['type'])
				{
					case 'TITLE':
						if (isset($data_current['title']))
							$data_current['title'] .= $data;
						else
							$data_current['title'] = $data;
						break;
					
/*					case 'GUID': REMOVED FOR NOW */ 
					case 'LINK':
						if (isset($data_current['href']))
							$data_current['href'] .= $data;
						else
							$data_current['href'] = $data;
						break;

					case 'PUBDATE':
						$data_current['date'] = strtotime($data);
						break;

					default:
						break;
				}
			}
			
			if ($this->detected_feed == 'Atom' &&
			   ($parent == 'ENTRY' || $parent == 'FEED'))
			{
				switch($feed_current['type'])
				{
					case 'TITLE':
						if (isset($data_current['title']))
							$data_current['title'] .= $data;
						else
							$data_current['title'] = $data;
						break;
					
					case 'CREATED':
						$data_current['date'] = $this->_iso8601($data);
						break;

					case 'MODIFIED':
						$data_current['date'] = $this->_iso8601($data);
						break;

					default:
						break;
				}
			}
		}
		else
		{
			// Handle Dublin Core extentions...
			if ($this->detected_feed == 'RSS/1.0' &&
				$feed_current['ns'] == 'HTTP://PURL.ORG/DC/ELEMENTS/1.1/')
			{
				switch($feed_current['type'])
				{
					case 'DATE':
						$data_current['date'] = $this->_iso8601($data);
						break;

					default:
						break;
				}
			}
		}
	}
}




class RSSAtomParserCache extends RSSAtomParser {
   /* ----------------------
	* Wrapper around RSSAtomParser that provides caching
	*/

	var $cache_ttl;
	var $cache_folder;

	function RSSAtomParserCache($ttl = 30, $dir = '/tmp') {
		$this->cache_ttl = $ttl;
		$this->cache_folder = $dir;
		mt_srand($this->_make_seed());
		
		// Initialize the parser
		$this->RSSAtomParser();
	}
	
	function setTTL ($ttl) {
		$this->cache_ttl = $ttl;
	}
	
	function setFolder ($dir) {
		$this->cache_folder = $dir;
	}
	
	function readURL ($feedurl) {
		$rebuild_cache = false;

		if ($this->_cache_exists($feedurl))
		{
			// Check if cache is valid
			$max    = ceil($this->cache_ttl / 2);
			$random = mt_rand (0, $max);
			$random = $random - ceil($max / 2);

			$filename = 'rssatom-'.md5($feedurl).".cache";
			$mtime = filemtime($this->cache_folder.'/'.$filename);

			$this->_debug(sprintf("Cache TTL is initially %d", $this->cache_ttl));
			$this->_debug(sprintf("Random element is %d", $random));
			$this->_debug(sprintf("Cache TTL is finally %d", $this->cache_ttl - $random));
			$this->_debug(sprintf("Cache for %s exists", $feedurl));
			$this->_debug(sprintf("Cache creation: %s",filectime($this->cache_folder.'/'.$filename)));
			$this->_debug(sprintf("Cache last modification: %s",$mtime));
			$this->_debug(sprintf("Cache last access: %s",$mtime));
			$this->_debug(sprintf("Current time: %s",time()));
			$this->_debug(sprintf("Cache is %s minutes old", ceil((time() - $mtime) / 60)));

			$rebuild_cache = ceil((time() - $mtime) / 60) >= ($this->cache_ttl - $random);
		}
		else
		{
			// Create a new cache
			$this->_debug(sprintf("Cache for %s does not exist", $feedurl));
			$rebuild_cache = true;
		}
		
		
		if ($rebuild_cache)
		{
			$this->_debug("Rebuilding the cache...");
			
			if ($this->_cache_exists($feedurl)) 
			{
				$this->_debug("Checking to make sure cache has expired...");

				$cache = $this->_read_cache($feedurl);

				if (isset($cache['etag']) && $cache['etag'] != '')
				{
					$this->_debug("Feed uses Etag [".$cache['etag']."]...");
					$this->_debug("Retrieving feed to make sure...");
		
					if ($this->debug) $this->startTimer();

					$reader = new XMLParserRetrieve();
					$reader->setUrl ($feedurl);
					$reader->setEtag ($cache['etag']);

					if ($this->debug) 
					{
						$time = $this->stopTimer();
						$this->_debug("Retrieve data time: ".$time);
					}

					$temp 	  = $reader->getContents();
					$response = $reader->getResponse();
					$etag     = $reader->getEtag();
					$encoding = $reader->getCharset();
					
					$this->_debug(sprintf ("Response %d", $response));
					$this->_debug(sprintf ("Server character set: %s", $encoding));

					switch ($response)
					{
						case '200':	
							// Parse new data
							$this->_debug("Current cache has indeed expired");
							$output = RSSAtomParser::_parse($temp, $etag, $feedurl, $encoding);
							$this->_write_cache($feedurl, $output);
							break;
							
						case '304':
							// Apparently it is still up to data
							$this->_debug("Current cache still up to date");
							$output = $cache;
							$this->_write_cache($feedurl, $cache);
							break; 
							
						default:
							// Apparently an error occurred
							$this->_debug("Could not retrieve feed");
							$this->_error("Could not retrieve feed");
							$output = $cache;
							break;
					}
				}
				else
				{
					$this->_debug("Etag not used, calling RSS/Atom Parser to retrieve fresh feed...");

					if ($output = RSSAtomParser::readURL($feedurl))
						$this->_write_cache($feedurl, $output);
					else
						$output = array();
				}
			}
			else
			{
				$this->_debug("Calling RSS/Atom Parser to retrieve a new feed...");

				if ($output = RSSAtomParser::readURL($feedurl))
					$this->_write_cache($feedurl, $output);
				else
					$output = array();
			}
		}
		else
		{
			$this->_debug("Using the cache...");
			$output = $this->_read_cache($feedurl);
		}

		return $output;
	}
	
	function _read_cache($url) {
		$cache_filename = 'rssatom-'.md5($url).".cache";
		return unserialize(@file_get_contents ($this->cache_folder.'/'.$cache_filename));
	}
	
	function _write_cache($url, $contents) {
		$cache_filename = 'rssatom-'.md5($url).".cache";

		if (@file_exists($this->cache_folder.'/'.$cache_filename))
		{
			$this->_debug("Unlinking existing cache");
			@unlink($this->cache_folder.'/'.$cache_filename);
		}	
		
		if ($fp = @fopen($this->cache_folder.'/'.$cache_filename, 'w+'))
		{
			$this->_debug("Storing feed in cache");
			fwrite($fp, serialize($contents));
			fclose($fp);
		}
		else
		{
			$this->_debug("Failed to write to cache");
			$this->_error(sprintf ("The cache directory '%s' is not writable", $this->cache_folder));
		}
	}
	
	function _cache_exists($url) {
		$cache_filename = 'rssatom-'.md5($url).".cache";
		return @file_exists($this->cache_folder.'/'.$cache_filename);
	}
	
	function _make_seed() {
	   list($usec, $sec) = explode(' ', microtime());
	   return (float) $sec + ((float) $usec * 100000);
	}
}


?>