<?php
/*!
@file class.rss20.php
@short klasse voor het lezen (en cachen) van RSS-bestanden
@date 2005-03-15 13:30
@author Thomas van den Heuvel aka FangorN - the@stack.nl
*/
/*
http://www.zend.com/zend/art/parsing.php
http://www.sitepoint.com/article/php-xml-parsing-rss-1-0
http://www.php.net/manual/en/function.xml-set-character-data-handler.php
PHP maakt gebruik van expat, een event-driven XML parser (in tegenstelling tot een tree-based parser)
expat maakt gebruik van events (geregistreerde callback-functies worden aangeroepen wanneer er een
event optreed, bijvoorbeeld als er een tag geopend op gesloten wordt)
Het is de verantwoordelijkheid van de programmeur om tijdens het parsen een stack of lijst bij te houden
waarin de structuur van het te parsen XML-bestand wordt vastgelegd.
*/
//! rss20 class
/*!
Met deze klasse kun je RSS-bestanden lezen (en cachen). De inhoud van het RSS-bestand wordt opgeslagen
in een array (binnen het rss20-object) die eenvoudig gebruikt kan worden om de headlines van een RSS-
bestand af te drukken.
*/
class rss20 {
var $file; //!< (string) XML-bestand dat geparsed dient te worden (URL)
var $cached_file; //!< (string) naam van het XML-bestand dat gecached dient te worden / waaruit gelezen wordt
var $from_file; //!< (bool) boolean die aangeeft of het XML-bestand werd gelezen van de site of uit cache
var $site_retry; //!< (bool) boolean die aangeeft of de laatst gecachede versie van het XML-bestand leeg was
var $rss; //!< (object) het XML-parser object
var $stack = array(); //!< (array) stack van het XML-bestand (wordt gebruikt voor het parsen, houdt geopende tags bij)
var $content = ""; //!< (string) buffer voor CDATA (character-data)
var $channel_tags = array("title", "link", "description", "language", "copyright", "managingEditor", "webMaster", "pubDate", "lastBuildDate", "category", "generator", "docs"); //!< (array) lijst van channel-tags die opgeslagen dienen te worden
var $item_tags = array("title", "link", "description", "author", "category", "pubDate", "content:encoded"); //!< (array) lijst van item-tags die opgeslagen dienen te worden
var $output = array(); //!< (array) de output van het XML-bestand wordt hier in opgeslagen (om dit array draait deze klasse in feite)
var $cur_chan = -1; //!< (int) huidige kanaal
var $cur_item = -1; //!< (int) huidige item
//! constructor
/*!
@post een parser voor het parsen van het XML-bestand is aangemaakt en geinitialiseerd
*/
function rss20() {
$this->rss = xml_parser_create(); // creeer de parser
xml_set_object($this->rss, $this); // geeft aan dat de XML-parser in het rss20-object staat
xml_parser_set_option($this->rss, XML_OPTION_CASE_FOLDING, false); // uppercasing in XML? XML is case-sensitive!
xml_parser_set_option($this->rss, XML_OPTION_SKIP_WHITE, true); // slaat "space characters" over in het XML-bestand
xml_set_element_handler($this->rss, "startelement", "endelement"); // callback functie voor openings- and sluitings-tags
xml_set_character_data_handler($this->rss, "characterdata"); // callback functie voor CDATA (character-data) binnenin tags
}
//! initialisatie functie
/*!
@param $file (string) het XML-bestand dat ingelezen dient te worden (URL)
@param $cache (string) wanneer je het XML-bestand wilt cachen, dient dit veld de naam van de lokale kopie te bevatten (default "")
@post het rss20-object is nu geinitialiseerd en gereed voor parse-data
*/
function newfile($file, $cache = "") {
$this->cur_chan = -1;
$this->cur_item = -1;
$this->file = $file;
$this->cached_file = $cache;
$this->from_file = false;
$this->site_retry = false;
$this->output['channel'] = array();
}
//! callback functie die wordt uitgevoerd wanneer er een tag wordt geopend
/*!
@param $rss (object) verwijzing naar de XML-parser
@param $name (string) de naam van de tag die er voor zorgde dat deze methode werd aangeroepen
@param $attrs (array) associatief array met de attributen+waarden van de tag (als die er zijn, RSS heeft er iig geen)
*/
function startelement($rss, $name, $attrs) {
// er is een nieuwe tag geopend - zet de naam van de openings-tag op de stack
array_push($this->stack, $name);
switch($name) {
case "channel":
// maak een nieuw channel aan
$this->cur_chan++; // verhoog de channel-teller
$this->output['channel'][$this->cur_chan] = array(); // maak een array voor dit channel
$this->output['channel'][$this->cur_chan]['item'] = array(); // maak een nieuw items-array voor binnen dit channel
$this->cur_item = -1; // reset het aantal items voor dit kanaal
break;
case "item":
$this->cur_item++; // verhoog de item-counter
break;
}
}
//! callback functie die wordt uitgevoerd wanneer er een tag wordt afgesloten
/*!
@param $rss (object) verwijzing naar de XML-parser
@param $name (string) de naam van de tag die er voor zorgde dat deze methode werd aangeroepen
*/
function endelement($rss, $name) {
/*
We zijn klaar met het lezen van (de inhoud van) een tag - $this->content bevat de inhoud van
de tag. Bepaal aan de hand van de laatst geopende tag op $this->stack wat de bedoeling is.
De tags "title", "link" en "description" kunnen zowel behoren tot een channel als een item.
Maak gebruik van de stack om te bepalen of we ons in een channel(tag) of item(tag) bevinden.
*/
// Mysterieuze spaties en regelovergangen voor en na de content ?
if(in_array("item", $this->stack) && in_array($name, $this->item_tags)) {
// We zitten in een item-tag en we zijn geinteresseerd in deze specifieke tag
$this->output['channel'][$this->cur_chan]['item'][$this->cur_item][$name] = $this->cdata(trim($this->content));
} else {
// We zitten in een channel-tag
if(in_array($name, $this->channel_tags)) {
// We zijn geinteresseerd in deze specifieke tag
$this->output['channel'][$this->cur_chan][$name] = $this->cdata(trim($this->content));
}
}
// Reset de tag-inhoud
$this->content = "";
// De tag is afgesloten, dus deze kan verwijderd worden van de stack
array_pop($this->stack);
}
//! callback functie die wordt uitgevoerd wanneer er CDATA (character-data) wordt gelezen
/*!
@param $rss (object) verwijzing naar de XML-parser
@param $data (string) data die tussen een openings- en sluitingstag staat
*/
function characterdata($rss, $data) {
$this->content .= $data; // verzamel character-data in $this->content (niet alle tekst tussen de tags wordt in één keer gelezen)
}
//! leest de feed afkomstig uit ofwel een lokale file of een externe URL, wat er voor zorgt dat de callback-functies worden aangeroepen
/*!
@post de feed is geparsed en opgeslagen in lokale object-variabelen (tenzij de feed niet "well-formed" was)
*/
function parse() {
$data_temp = $this->getfeed($this->file, $this->cached_file);
if(!xml_parse($this->rss, $data_temp)) {
die(sprintf("XML error: %s at line %d", xml_error_string(xml_get_error_code($this->rss)), xml_get_current_line_number($this->rss)));
}
}
//! retourneert het aantal channels
/*!
@return (int) bevat het aantal channels dat de feed heeft
*/
function get_number_of_channels() {
return sizeof($this->output['channel']);
}
//! retourneert het aantal items van een bepaald channel
/*!
@param $channel (int) het channel-nummer waarvan we het aantal items willen weten
@return (int) bevat het aantal items dat een specifiek channel heeft
*/
function get_number_of_items($channel) {
return sizeof($this->output['channel'][$channel]['item']);
}
//! retourneert informatie van een specifiek kanaal
/*!
@param $channel (int) het channel-nummer waarvan we informatie willen hebben
@return (array) associatief array met hierin channel-informatie
*/
function get_channel_info($channel) {
$ret = array();
foreach($this->output['channel'][$channel] as $k => $v) {
if($k != "item") {
$ret[$k] = $v;
}
}
return $ret;
}
//! retouneert alle items van een bepaald channel - dit is de methode waar het in deze klasse in feite om draait
/*!
@param $channel (int) het channel-nummer waar we de items van willen hebben
@return (array) associatief array met hierin alle items van het channel $channel
*/
function get_items($channel) {
return $this->output['channel'][$channel]['item'];
}
//! deze methode zou aangeroepen moeten worden na het parsen van een XML-file - je moet ook een nieuw rss20-object aanmaken als je meerdere RSS-bestanden wilt parsen in hetzelfde script
/*!
@post de parser is vrijgegeven (waarmee in feite het rss20-object is "opgebruikt")
*/
function free_parser() {
xml_parser_free($this->rss);
}
//! stript de <![CDATA[...]]> tag en / of spaties uit de item-tags
/*!
@param $input (string) inhoud van een tag-paar (mogelijk omringd door een <![CDATA[...]]> tag)
@return (string) getrimde inhoud van het tag-pair, ontdaan van de <![CDATA[...]]> tag als deze aanwezig was
*/
function cdata($input) {
// verwijder <![CDATA[content]]> uit $input
// trim() is ook nodig - anders blijf je zitten met (ongewenste) spaties die achter zijn gebleven in <![CDATA[...]]>
return preg_replace("/<!\[CDATA\[(.*)\]\]>/se", "trim('\\1')", $input);
}
//! leest een XML-feed van een URL zonder de pagina (al te lang :)) te laten hangen - www.php.net/file - function fetchUrlWithoutHanging()
/*!
@param $url (string) URL van de externe feed
@param $cache (string) naam van de file die gebruikt dient te worden voor caching, laat deze leeg als je de feed niet wilt cachen (default "")
@param $refresh (int) max leeftijd van de gecachede feed in seconden, als de leeftijd groter is dan $refresh seconden dan zal de feed gelezen worden van $url (default 900)
@return (string) de complete feed
NB: Deze methode is nog een beetje een work-in-progress, hier kunnen dus
waarschijnlijk nog zaken aan veranderd / verbeterd worden.
*/
function getfeed($url, $cache="", $refresh=900) {
$path = $cache; // het pad waar je je XML-bestand naar wegschrijft in geval van caching - zorg dat dit klopt !
/*
Dient de feed uit cache gelezen te worden ?
filesize($path) == 0 zou kunnen aangeven dat de site plat was/is (of de socket-connectie is ge-timeout)
we willen iig niet $timeout seconden wachten om het nog eens te proberen
*/
if($cache != "" && file_exists($path) && time() - filemtime($path) < $refresh && filesize($path) > 0) {
$this->from_file = true; // de feed wordt uit cache gelezen
$handle = fopen($path, "r");
$size = (filesize($path) > 0) ? filesize($path) : 1024; // empty file fix
$return = fread($handle, $size);
fclose($handle);
}
if($cache == "" ||
!file_exists($path) ||
(file_exists($path) && time() - filemtime($path) >= $refresh) ||
($cache != "" && file_exists($path) && filesize($path) == 0)) {
// De file was niet gecached, bestond nog niet, is verlopen of is leeg, update het bestand
// $this->from_file stond al op false, dit geeft aan dat het XML-bestand gelezen wordt van een externe URL
// Was het een tweede poging ?
if(file_exists($path) && filesize($path) == 0 && $cache != "") {
$this->site_retry = true;
}
/*
Het volgende deel is een ietwat aangepaste variant van de fetchUrlWithoutHanging() functie
*/
// Set maximum number of seconds (can have floating-point) to wait for feed before displaying page without feed
// retrieving a feed can take (far) longer than this - cause ?
$numberOfSeconds = 5;
// Suppress error reporting so Web site visitors are unaware if the feed fails
error_reporting(0);
// Extract resource path and domain from URL ready for fsockopen
$url = str_replace("http://", "", $url);
$urlComponents = explode("/", $url);
$domain = $urlComponents[0];
$resourcePath = str_replace($domain, "", $url);
// Establish a connection - may take 5 seconds
$socketConnection = fsockopen($domain, 80, $errno, $errstr, $numberOfSeconds);
// modification (doesn't seem to do much though :))
stream_set_blocking($socketConnection, false);
stream_set_timeout($socketConnection, $numberOfSeconds); // connection may take 5 seconds
if(!$socketConnection) {
// you may wish to remove the following debugging line on a live Web site
// print("<!-- Network error: $errstr ($errno) -->");
} else {
$xml = "";
fputs($socketConnection, "GET /".$resourcePath." HTTP/1.0\r\nHost: ".$domain."\r\n\r\n");
// loop until end of file
while (!feof($socketConnection)) {
$xml .= fgets($socketConnection, 128);
} // end while
fclose($socketConnection);
}
// Aanpassing - sloop headers eruit
preg_match("/(<\?xml|<rss)(.*)/s", $xml, $content);
$return = $content[0];
// het volgende stuk code kan op sommige webservers problemen opleveren, dit ligt waarschijnlijk niet aan dit script
// Dient de feed naar cache geschreven te worden ?
if($cache != "") {
$handle = fopen($path, "w");
fwrite($handle, $return);
fclose($handle);
chmod($path, 0644); // dit kan op sommige webservers problemen opleveren, dit ligt niet aan dit script
}
}
/*
Retourneer alleen de content. Als de content leeg is, dan is de URL naar de feed waarschijnlijk
verkeerd (403 - moved permanently)
Voor alle content (inclusief headers) retourneer $xml
*/
return $return;
}
//! geeft aan of de feed uit cache werd gelezen (true) of uit een externe file (false)
/*!
@return (bool) boolean die aangeeft of de feed uit een lokaal bestand werd gelezen
*/
function read_from_file() {
return $this->from_file;
}
//! geeft aan of de laatste gecachede file leeg was - dit zou kunnen inhouden dat de site vanwaar de feed afkomstig is plat ligt
/*!
@return (bool) boolean die aangeeft of de laatst gecachede file leeg was
*/
function read_site_retry() {
return $this->site_retry;
}
}
?>
<?php
/*!
@file class.rss20.php
@short klasse voor het lezen (en cachen) van RSS-bestanden
@date 2005-03-15 13:30
@author Thomas van den Heuvel aka FangorN - the@stack.nl
PHP maakt gebruik van expat, een event-driven XML parser (in tegenstelling tot een tree-based parser)
expat maakt gebruik van events (geregistreerde callback-functies worden aangeroepen wanneer er een
event optreed, bijvoorbeeld als er een tag geopend op gesloten wordt)
Het is de verantwoordelijkheid van de programmeur om tijdens het parsen een stack of lijst bij te houden
waarin de structuur van het te parsen XML-bestand wordt vastgelegd.
*/
//! rss20 class
/*!
Met deze klasse kun je RSS-bestanden lezen (en cachen). De inhoud van het RSS-bestand wordt opgeslagen
in een array (binnen het rss20-object) die eenvoudig gebruikt kan worden om de headlines van een RSS-
bestand af te drukken.
*/
class rss20 {
var$file;//!< (string) XML-bestand dat geparsed dient te worden (URL)
var$cached_file;//!< (string) naam van het XML-bestand dat gecached dient te worden / waaruit gelezen wordt
var$from_file;//!< (bool) boolean die aangeeft of het XML-bestand werd gelezen van de site of uit cache
var$site_retry;//!< (bool) boolean die aangeeft of de laatst gecachede versie van het XML-bestand leeg was
var$rss;//!< (object) het XML-parser object
var$stack=array();//!< (array) stack van het XML-bestand (wordt gebruikt voor het parsen, houdt geopende tags bij)
var$content="";//!< (string) buffer voor CDATA (character-data)
var$channel_tags=array("title","link","description","language","copyright","managingEditor","webMaster","pubDate","lastBuildDate","category","generator","docs");//!< (array) lijst van channel-tags die opgeslagen dienen te worden
var$item_tags=array("title","link","description","author","category","pubDate","content:encoded");//!< (array) lijst van item-tags die opgeslagen dienen te worden
var$output=array();//!< (array) de output van het XML-bestand wordt hier in opgeslagen (om dit array draait deze klasse in feite)
var$cur_chan=-1;//!< (int) huidige kanaal
var$cur_item=-1;//!< (int) huidige item
//! constructor
/*!
@post een parser voor het parsen van het XML-bestand is aangemaakt en geinitialiseerd
//! retouneert alle items van een bepaald channel - dit is de methode waar het in deze klasse in feite om draait
/*!
@param $channel (int) het channel-nummer waar we de items van willen hebben
@return (array) associatief array met hierin alle items van het channel $channel
*/
function get_items($channel){
return$this->output['channel'][$channel]['item'];
}
//! deze methode zou aangeroepen moeten worden na het parsen van een XML-file - je moet ook een nieuw rss20-object aanmaken als je meerdere RSS-bestanden wilt parsen in hetzelfde script
/*!
@post de parser is vrijgegeven (waarmee in feite het rss20-object is "opgebruikt")
//! leest een XML-feed van een URL zonder de pagina (al te lang :)) te laten hangen - www.php.net/file - function fetchUrlWithoutHanging()
/*!
@param $url (string) URL van de externe feed
@param $cache (string) naam van de file die gebruikt dient te worden voor caching, laat deze leeg als je de feed niet wilt cachen (default "")
@param $refresh (int) max leeftijd van de gecachede feed in seconden, als de leeftijd groter is dan $refresh seconden dan zal de feed gelezen worden van $url (default 900)
@return (string) de complete feed
NB: Deze methode is nog een beetje een work-in-progress, hier kunnen dus
waarschijnlijk nog zaken aan veranderd / verbeterd worden.
*/
function getfeed($url,$cache="",$refresh=900){
$path=$cache;// het pad waar je je XML-bestand naar wegschrijft in geval van caching - zorg dat dit klopt !
/*
Dient de feed uit cache gelezen te worden ?
filesize($path) == 0 zou kunnen aangeven dat de site plat was/is (of de socket-connectie is ge-timeout)
we willen iig niet $timeout seconden wachten om het nog eens te proberen
<?php
/////////////////////////////////////////////////////////////////////////////////
// voorbeeld #1 //
// lees een feed zonder caching //
/////////////////////////////////////////////////////////////////////////////////
require("class.rss20.php"); // include class-file
$rss = new rss20(); // maak een rss-object aan
$rss->newfile("http://www.sitemasters.be/rss/xml/nieuws.xml"); // lees een externe feed (zonder caching)
$rss->parse(); // parse de XML-file
// druk de items af
?>
<b>sitemasters.be - nieuws</b> (<?=$rss->get_number_of_items(0) ?> items)<br />
<?php
$items = $rss->get_items(0); // aantal items in channel 0
// de informatie die je wilt afdrukken hangt af van wat er in de feed aangeboden wordt en
// wat je zelf wilt laten zien, deze zal dus niet altjd hetzelfde zijn
for($i=0; $i < sizeof($items); $i++) {
echo date("Y-m-d H:i", strtotime($items[$i]['pubDate']))." - ";
echo "<a href=\"".$items[$i]['link']."\" target=\"_blank\">".$items[$i]['title']."</a> ";
echo "door ".htmlentities($items[$i]['author'])."<br />\n";
}
$rss->free_parser(); // geef de parser weer vrij
unset($rss); // als je meerdere feeds op één pagina wilt zetten moet je het object unsetten (of een andere var-naam gebruiken)
/////////////////////////////////////////////////////////////////////////////////
// voorbeeld #2 //
// lees een feed met caching (cache het bestand als "sitemasters.scripts.xml") //
/////////////////////////////////////////////////////////////////////////////////
$rss = new rss20();
$rss->newfile("http://www.sitemasters.be/rss/xml/scripts.xml", "sitemasters.scripts.xml");
$rss->parse();
?>
<br />
<b>sitemasters.be - scripts</b> (<?=$rss->get_number_of_items(0) ?> items)<br />
<?php
$items = $rss->get_items(0);
for($i=0; $i < sizeof($items); $i++) {
echo date("Y-m-d H:i", strtotime($items[$i]['pubDate']))." - <a href=\"".$items[$i]['link']."\" target=\"_blank\">".$items[$i]['title']."</a><br />\n";
}
$from_file = $rss->read_from_file(); // wordt de file uit cache gelezen ?
$site_retry = $rss->read_site_retry(); // mislukte de laatste poging om de feed te cachen ?
$rss->free_parser(); // geef de parser vrij
unset($rss); // unset het RSS-object
echo "gelezen van ".($from_file ? "file" : "site");
echo ($site_retry ? " (retry)" : "")."<br /><br />\n";
?>