Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
311 changes: 311 additions & 0 deletions Feed.inc.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
<?php

/**
* RSS for PHP - small and easy-to-use library for consuming an RSS Feed
*
* @copyright Copyright (c) 2008 David Grudl
* @license New BSD License
* @version 1.5
*/
class Feed
{
/** @var int */
public static $cacheExpire = '1 day';

/** @var string */
public static $cacheDir;

/** @var string */
public static $userAgent = 'FeedFetcher-Google';

/** @var SimpleXMLElement */
protected $xml;


/**
* Loads RSS or Atom feed.
* @param string
* @param string
* @param string
* @return Feed
* @throws FeedException
*/
public static function load($url, $user = null, $pass = null)
{
$xml = self::loadXml($url, $user, $pass);
if ($xml->channel) {
return self::fromRss($xml);
} else {
return self::fromAtom($xml);
}
}


/**
* Loads RSS feed.
* @param string RSS feed URL
* @param string optional user name
* @param string optional password
* @return Feed
* @throws FeedException
*/
public static function loadRss($url, $user = null, $pass = null)
{
return self::fromRss(self::loadXml($url, $user, $pass));
}


/**
* Loads Atom feed.
* @param string Atom feed URL
* @param string optional user name
* @param string optional password
* @return Feed
* @throws FeedException
*/
public static function loadAtom($url, $user = null, $pass = null)
{
return self::fromAtom(self::loadXml($url, $user, $pass));
}


private static function fromRss(SimpleXMLElement $xml)
{
if (!$xml->channel) {
throw new FeedException('Invalid feed.');
}

self::adjustNamespaces($xml);

$channel = $xml->channel;

if(isset($xml->item)) {
// Some feeds have items at root, not under channel. Add them under channel.
// Basic procedure from https://stackoverflow.com/questions/5735857/php-domdocument-move-nodes-from-a-document-to-another
$xml_dom = dom_import_simplexml($channel);
while(count($xml->item) > 0) {
// It makes no sense, but this has the side effect of popping the item as off a stack.
$item = $xml->item[0];
$item_dom = dom_import_simplexml($item);
$item_dom_adopted = $xml_dom->ownerDocument->importNode($item_dom, true);
$xml_dom->appendChild($item_dom_adopted);
}
$channel = simplexml_import_dom($xml_dom);
}

foreach ($channel->item as $item) {
// converts namespaces to dotted tags
self::adjustNamespaces($item);

// generate 'url' & 'timestamp' tags
$item->url = (string) $item->link;
if (isset($item->{'dc:date'})) {
$item->timestamp = strtotime($item->{'dc:date'});
} elseif (isset($item->pubDate)) {
$item->timestamp = strtotime($item->pubDate);
}
}
$feed = new self;
$feed->xml = $channel;
return $feed;
}


private static function fromAtom(SimpleXMLElement $xml)
{
if (!in_array('http://www.w3.org/2005/Atom', $xml->getDocNamespaces(), true)
&& !in_array('http://purl.org/atom/ns#', $xml->getDocNamespaces(), true))
{
throw new FeedException('Invalid feed.');
}

// generate 'url' & 'timestamp' tags
foreach ($xml->entry as $entry) {
$entry->url = (string) $entry->link['href'];
$entry->timestamp = strtotime($entry->updated);
}
$feed = new self;
$feed->xml = $xml;
return $feed;
}


/**
* Returns property value. Do not call directly.
* @param string tag name
* @return SimpleXMLElement
*/
public function __get($name)
{
return $this->xml->{$name};
}


/**
* Sets value of a property. Do not call directly.
* @param string property name
* @param mixed property value
* @return void
*/
public function __set($name, $value)
{
throw new Exception("Cannot assign to a read-only property '$name'.");
}


/**
* Converts a SimpleXMLElement into an array.
* @param SimpleXMLElement
* @return array
*/
public function toArray(SimpleXMLElement $xml = null)
{
if ($xml === null)
{
$xml = $this->xml;
}

$ar = array();
foreach ($xml->children() as $k => $v)
{
$child = self::toArray($v);
if (count($child) == 0)
{
$child = (string) $v;
}
foreach ($v->attributes() as $ak => $av)
{
if (!is_array($child))
{
$child = array("value" => $child);
}
$child[$ak] = (string) $av;
}
if (!array_key_exists($k, $ar))
{
$ar[$k] = $child;
}
else
{
if (!is_string($ar[$k]) && isset($ar[$k][0]))
{
$ar[$k][] = $child;
}
else
{
$ar[$k] = array($ar[$k]);
$ar[$k][] = $child;
}
}
}
return $ar;
}


/**
* Load XML from cache or HTTP.
* @param string
* @param string
* @param string
* @return SimpleXMLElement
* @throws FeedException
*/
private static function loadXml($url, $user, $pass)
{
$e = self::$cacheExpire;
$cacheFile = self::$cacheDir . '/feed.' . md5(serialize(func_get_args())) . '.xml';

if (self::$cacheDir
&& (time() - @filemtime($cacheFile) <= (is_string($e) ? strtotime($e) - time() : $e))
&& $data = @file_get_contents($cacheFile)
) {
// ok
} elseif ($data = trim(self::httpRequest($url, $user, $pass))) {
if (self::$cacheDir) {
file_put_contents($cacheFile, $data);
}
} elseif (self::$cacheDir && $data = @file_get_contents($cacheFile)) {
// ok
} else {
throw new FeedException('Cannot load feed.');
}

return new SimpleXMLElement($data, LIBXML_NOWARNING | LIBXML_NOERROR | LIBXML_NOCDATA);
}


/**
* Process HTTP request.
* @param string
* @param string
* @param string
* @return string|false
* @throws FeedException
*/
private static function httpRequest($url, $user, $pass)
{
if (extension_loaded('curl')) {
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
if ($user !== null || $pass !== null) {
curl_setopt($curl, CURLOPT_USERPWD, "$user:$pass");
}
curl_setopt($curl, CURLOPT_USERAGENT, self::$userAgent); // some feeds require a user agent
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_TIMEOUT, 20);
curl_setopt($curl, CURLOPT_ENCODING, '');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); // no echo, just return result

if (!ini_get('open_basedir')) {
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); // sometime is useful :)
}
$result = curl_exec($curl);

return curl_errno($curl) === 0 && curl_getinfo($curl, CURLINFO_HTTP_CODE) === 200
? $result
: false;

} else {
$context = null;
if ($user !== null && $pass !== null) {
$options = [
'http' => [
'method' => 'GET',
'header' => 'Authorization: Basic ' . base64_encode($user . ':' . $pass) . "\r\n",
],
];
$context = stream_context_create($options);
}

return file_get_contents($url, false, $context);
}
}


/**
* Generates better accessible namespaced tags.
* @param SimpleXMLElement
* @return void
*/
private static function adjustNamespaces($el)
{
foreach ($el->getNamespaces(true) as $prefix => $ns) {
if ($prefix === '') {
continue;
}
$children = $el->children($ns);
foreach ($children as $tag => $content) {
$el->{$prefix . ':' . $tag} = $content;
}
}
}
}



/**
* An exception generated by Feed.
*/
class FeedException extends Exception
{
}
1 change: 1 addition & 0 deletions build.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

// build parameter string for the feed2js url
$options = '';
$html_options = '';
if ($chan != 'n') $options .= "&chan=$chan";
if ($num != 0) $options .= "&num=$num";
if ($desc != 0) $options .= "&desc=$desc";
Expand Down
1 change: 1 addition & 0 deletions cache/.WRITEABLE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This directory MUST be writeable by the web server process!
Loading