<?php

/**
 * THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @package fusionLib
 * @copyright Copyright (c) 2008 - 2016 fusionLib. All rights reserved.
 * @link http://fusionlib.com
 */

/**
 * flRssChannel Class
 *
 * Class to hold channel information.
 *
 * @package fusionLib
 */
class flRssChannel implements Iterator, Countable {
	/**
	 * Title.
	 *
	 * @var string
	 */
	public $title;

	/**
	 * Link.
	 *
	 * @var string
	 */
	public $link;

	/**
	 * Description.
	 *
	 * @var string
	 */
	public $description;

	/**
	 * Language.
	 *
	 * @var string
	 */
	public $language = '';

	/**
	 * Copyright.
	 *
	 * @var string
	 */
	public $copyright = '';

	/**
	 * Publication date.
	 *
	 * @var string
	 */
	public $pubDate = 0;

	/**
	 * Date of last build.
	 *
	 * @var string
	 */
	public $lastBuildDate = 0;

	/**
	 * Array of categories.
	 *
	 * @var array
	 */
	public $categories = array();

	/**
	 * TTL.
	 *
	 * @var int
	 */
	public $ttl = 60;

	/**
	 * Address of image.
	 *
	 * @var string
	 */
	public $image = '';

	/**
	 * The list of channel items.
	 *
	 * @var flRssItem
	 */
	public $items = array();

	/**
	 * Get the next item in the list.
	 *
	 * @return mixed|void The item.
	 */
	function next() { return next($this->items); }

	/**
	 * Move to the first item in the list.
	 *
	 * @return mixed|void The item.
	 */
	function rewind() { return reset($this->items); }

	/**
	 * Get the item key.
	 *
	 * @return mixed The key.
	 */
	function key() { return key($this->items); }

	/**
	 * Get the current item.
	 *
	 * @return mixed The item.
	 */
	function current() { return current($this->items); }

	/**
	 * Test if the location in the list is valid.
	 *
	 * @return bool true if valid; else false.
	 */
	function valid() { return current($this->items) !== false; }

	/**
	 * Get the number of items in the channel.
	 * @return int Number of items.
	 */
	function count() { return count($this->items); }
}

/**
 * flRssItem Class
 *
 * Class to hold a channel item.
 *
 * @package fusionLib
 */
class flRssItem implements Iterator, Countable {
	/**
	 * Elements.
	 *
	 * @var array
	 */
	public $elements = array();

	/**
	 * Categories.
	 *
	 * @var array
	 */
	public $categories = array();

	/**
	 * Read a value.
	 *
	 * @param string $key The name of the element to get.
	 * @return string The value or and empty string if not set.
	 */
	function __get($key) {
		return isset($this->elements[$key]) ? $this->elements[$key] : '';
	}

	/**
	 * Test if a element is present.
	 *
	 * @param string $key The name of the element to test for.
	 * @return bool True if the element is present; else false.
	 */
	public function __isset($key) {
		return isset($this->elements[$key]);
	}

	/**
	 * Set a new value for an element.
	 *
	 * @param string $key The name of the element to set.
	 * @param mixed $value The value to set.
	 */
	function __set($key, $value) {
		$this->elements[$key] = $value;
	}

	/**
	 * Unset an element.
	 *
	 * @param string $key The name of the element to unset.
	 */
	function __unset($key) {
		unset($this->elements[$key]);
	}

	/**
	 * Get the next item.
	 *
	 * @return mixed|void The item.
	 */
	function next() { return next($this->elements); }

	/**
	 * Move to the first item in the list.
	 *
	 * @return mixed|void The item.
	 */
	function rewind() { return reset($this->elements); }

	/**
	 * Get the key.
	 *
	 * @return mixed The key.
	 */
	function key() { return key($this->elements); }

	/**
	 * Get the current item.
	 *
	 * @return mixed The item.
	 */
	function current() { return current($this->elements); }

	/**
	 * Test if the current location is valid.
	 *
	 * @return bool true if valid; else false.
	 */
	function valid() { return current($this->elements) !== false; }

	/**
	 * Get the number of items.
	 *
	 * @return int Number of items.
	 */
	function count() { return count($this->elements); }
}

/**
 * flRssClient Class
 *
 * Class to implement fetching and caching of RSS feeds.
 *
 * Feeds are cached with the tag rss.
 *
 * @package fusionLib
 */
class flRssClient {

	/**
	 * The cache object to use.
	 *
	 * @var flCache
	 */
	private $cache;

	/**
	 * Create the feed object.
	 *
	 * @param flCache The cache object to use for caching the feed, null can be used to disable caching.
	 */
	function __construct($cache) {
		$this->cache = $cache;
	}

	/**
	 * Fetch the RSS feed from the given URI.
	 *
	 * @param string $uri The URI of the feed to fetch.
	 * @param int $maxItems The maximum number of channel items to return.
	 * @param bool $withCategories True if categories should be read and returned or false.
	 * @param int $cacheTime The number of minutes to cache the feed for, if null the feed is cached
	 * for the ttl value set in the feed or if not set 60 minutes.
	 * @return flRssChannel The data read from the channel or false if the channel could not be read.
	 */
	function fetch($uri, $maxItems = 50, $withCategories = false, $cacheTime = null) {
		$channel = false;

		$cacheID = 'flRssClient_' . $uri . $maxItems . $withCategories . $cacheTime;
		if($this->cache === null || ($channel = $this->cache->get($cacheID)) === false) {

			// Create the HTTP object and fetch the feed
			$http = new flHttpClient($uri);
			$http->setTimeout(5);
			$http->addHeader('User-Agent', 'fusionLib ' . fusionLib::getVersion() . ' RSS Client');
			if($http->sendRequest() && $http->getResponseCode() == 200) {

				// Parese the XML
				if(($xml = simplexml_load_string($http->getBody(), 'SimpleXMLElement', LIBXML_NOCDATA | LIBXML_NOERROR | LIBXML_NOENT)) !== false) {
					$ns = $xml->getNamespaces(true);

					// Check if feed looks valid RSS
					if(!isset($xml->channel) || !isset($xml->channel->title) || !isset($xml->channel->link) || !isset($xml->channel->item)) {
						// see if it looks like a valid atom feed
						if(isset($xml->title) && isset($xml->link) && isset($xml->entry))
							$channel = $this->processAtomFeed($xml, $maxItems, $cacheTime);
						else
							return false;
					}
					else {
						// Create the channel and fill it in
						$channel = new flRssChannel();
						$channel->title = flTextStrip::tags($xml->channel->title);
						$channel->link = flTextStrip::tags($xml->channel->link);
						$channel->description = isset($xml->channel->description) ? flTextStrip::tags($xml->channel->description) : '';

						if(isset($xml->channel->language)) $channel->language = flTextStrip::tags($xml->channel->language);
						if(isset($xml->channel->copyright)) $channel->copyright = flTextStrip::tags($xml->channel->copyright);
						if(isset($xml->channel->pubDate)) $channel->pubDate = (string)$xml->channel->pubDate;
						if(isset($xml->channel->lastBuildDate)) $channel->lastBuildDate = (string)$xml->channel->lastBuildDate;

						if($cacheTime !== null)
							$channel->ttl = $cacheTime;
						else if(isset($xml->channel->ttl))
							$channel->ttl = isset($xml->channel->ttl) ? flTextStrip::tags($xml->channel->ttl) : 60;

						if(isset($xml->channel->image)) $channel->image = flTextStrip::tags($xml->channel->image);
						if($withCategories && isset($xml->channel->category)) {
							foreach($xml->channel->category as $cat)
								$channel->categories[] = flTextStrip::tags($cat);
						}

						// Get the items
						foreach($xml->channel->item as $item) {
							$i = new flRssItem();

							// Save data
							foreach($item as $k => $v) {
								// Global name space items
								if ($k == 'description')
									$i->elements[$k] = (string)$v;
								elseif ($k == 'pubDate')
									$i->elements[$k] = new flDate((string)$v);
								elseif ($k == 'category') {
									if ($withCategories) {
										foreach ($item->category as $cat)
											$i->categories[] = flTextStrip::tags($cat);
									}
								} else {
									if (!strlen($v)) {
										$a = $v->attributes();
										$i->elements[$k] = isset($a->url) ? (string)$a->url : '';
									} else
										$i->elements[$k] = flTextStrip::tags($v);
								}
							}

							// Get name space data
							foreach($ns as $n => $nsn) {
								$nsData = $item->children($nsn);
								foreach($nsData as $nk => $nv) {
									if(!strlen($nv)) {
										$a = $nv->attributes();
										$i->elements[$n . '_' . $nk] = isset($a->url) ? (string)$a->url : '';
									}
									else
										$i->elements[$n . '_' . $nk] = empty($nv) ? 'na' : flTextStrip::tags($nv);
								}
							}

							if(!isset($i->elements['pubDate']))
								$i->elements['pubDate'] = new flDate();

							// Save channel item
							$channel->items[] = $i;

							if(count($channel->items) >= $maxItems)
								break;
						}
					}

					// Cache the data
					if($this->cache !== null)
						$this->cache->store($cacheID, $channel, $channel->ttl * 60);
				}
			}
		}

		return $channel;
	}

	/**
	 * Process XML data holding an Atom format feed.
	 *
	 * @param SimpleXMLElement $xml The XML data fetched from the feed server.
	 * @param int $maxItems The maximum number of channel items to return.
	 * @param int $cacheTime The number of minutes to cache the feed for, if null the feed is cached
	 * for the ttl value set in the freed or if not set 60 minutes.
	 * @return flRssChannel The data read from the channel or false if the channel could not be read.
	 */
	protected function processAtomFeed($xml, $maxItems, $cacheTime) {
		// Create the channel and fill it in
		$channel = new flRssChannel();
		$channel->title = flTextStrip::tags($xml->title);
		$l = $xml->link->attributes();
		$channel->link = flTextStrip::tags($l['href']);
		$channel->description = '';

		if(isset($xml->updated)) $channel->lastBuildDate = (string)$xml->updated;
		$channel->ttl = $cacheTime === null ? 60 : $cacheTime;

		// Get the items
		foreach($xml->entry as $item) {
			$i = new flRssItem();

			// Save data
			foreach($item as $k => $v) {
				// Global name space items
				if($k == 'title')
					$i->elements['title'] = (string)$v;
				elseif($k == 'summary')
					$i->elements['description'] = (string)$v;
				elseif($k == 'published')
					$i->elements['pubDate'] = new flDate((string)$v);
				elseif($k == 'link') {
					$a = $v->attributes();
					$i->elements['link'] = flTextStrip::tags($a['href']);
				}
				elseif($k == 'id')
					$i->elements['guid'] = (string)$v;
			}

			if(!isset($i->elements['pubDate']))
				$i->elements['pubDate'] = new flDate();

			// Save channel item
			$channel->items[] = $i;

			if(count($channel->items) >= $maxItems)
				break;
		}

		return $channel;
	}
}
