. * * @version $Id: index.php 68 2007-07-26 16:42:19Z epsi $ * @author Stephane Daury: http://stephane.daury.org/ * @license GNU Public License: http://www.gnu.org/licenses/gpl.html */ new parseMe(); class parseMe { /** * Admin-defined configurable options. * Associative array containing the application-level settings. * @var array */ private $_config = array( 'appName' => 'parseMe', 'cache' => array( 'directory' => './cache', 'timeToLive' => '30 minutes', 'secret' => '' ), 'xmlSourceList' => array( 'tekArtist' => 'http://feeds.feedburner.com/tekArtist', 'Digg: Tech.' => 'http://digg.com/rss/containertechnology.xml', 'Slashdot' => 'http://rss.slashdot.org/Slashdot/slashdot', 'Google News' => 'http://news.google.com/?output=rss' ), 'limits' => array( 'increment' => 5, 'max' => 25, 'default' => 5, 'description' => 1024 ), 'order' => 'feed', 'googleMobile' => true ); /** * User-agent for connections, updated in __construct(). * @var string */ private $_userAgent = 'parseMe'; /** * Cookie name for source preferences. * @var string */ private $_cookieDefaultSources = 'parseMe_defaultSource'; /** * Cookie name for items-per-page preferences. * @var string */ private $_cookieDefaultLimit = 'parseMe_defaultLimit'; /** * Cookie name for sorting preferences. * @var string */ private $_cookieDefaultOrder = 'parseMe_defaultOrder'; /** * Cookie name for Google Mobile Gateway preferences. * @var string */ private $_cookieDefaultGoogle = 'parseMe_defaultGoogle'; /** * Cache file modification time (Epoch). * @var integer */ private $_cacheMtime = 0; /** * Associative array containing the unified data to display, regardless * of the feed(s) type. * @var array */ private $_feedData = array(); /** * date() format string used to generate keys for $this->_feedData * @var string */ private $_feedDataKeyFormat = 'Y-m-d H:i:s'; /** * Valid sorting options * @var array */ private $_orderTypes = array( 'feed' => 'By feeds', 'desc' => 'Newest first', 'asc' => 'Oldest first' ); /** * parseMe homepage, for credits * @var string */ private $_parseMeLink = 'http://www.tekartist.org/labs/parseme/'; /** * Initializes custom user-agent and assigns values * * @param array Admin-defined application settings * @return void */ public function __construct() { $this->_userAgent = $this->_config['appName'].'('.$_SERVER['SCRIPT_URI'].')'; $this->_cacheMtime = time(); $this->_init(); } /** * Gets the user or application default xml sources. * * @return array Array keys in $this->_config['xmlSourceList'] */ private function _getDefaultSources(){ $xmlSourceList = $this->_config['xmlSourceList']; $sourceKeys = array_keys($xmlSourceList); $defaultSource = $sourceKeys[0]; if(isset($_COOKIE[$this->_cookieDefaultSources])){ $userCookieSources = unserialize(base64_decode($_COOKIE[$this->_cookieDefaultSources])); if(is_array($userCookieSources)){ foreach($userCookieSources as $thisSource){ if(isset($xmlSourceList[$thisSource])){ $userDefaultSources[] = $thisSource; } } } } if(count($userDefaultSources) > 0){ return $userDefaultSources; }else{ return array($defaultSource); } } /** * Gets the user or application default items-per-page limit. * * @return integer */ private function _getDefaultLimit(){ $limit = intval($_COOKIE[$this->_cookieDefaultLimit]); if($limit > 0 && $limit <= $this->_config['limits']['max']){ return $limit; } else{ return $this->_config['limits']['default']; } } /** * Gets the user or application default sort option. * * @return string Array key in $this->_orderTypes. */ private function _getDefaultOrder(){ $order = $_COOKIE[$this->_cookieDefaultOrder]; if(isset($this->_orderTypes[$order])){ return $order; } else{ return $this->_config['order']; } } /** * Gets the user or application default Google Mobile Gateway pref. * * @return boolean */ private function _getDefaultGoogle(){ $useGoogle = unserialize(base64_decode($_COOKIE[$this->_cookieDefaultGoogle])); if( isset($_COOKIE[$this->_cookieDefaultGoogle]) && ($useGoogle === true || $useGoogle === false)){ return $useGoogle; } else{ return $this->_config['googleMobile']; } } /** * Generates fake date-based keys to be used with $this->_feedData, * when an item does not have a publication date. * * @return string Date as defined by $this->_feedDataKeyFormat */ private function _bogusDateKey(){ return date($this->_feedDataKeyFormat,rand(strtotime('yesterday'), time())).' [bogus]'; } /** * Feed parsing decisional wrapper. * * @param object SimpleXML object (http://php.net/simplexml) * @return void */ private function _parseFeed($xml){ if(isset($xml->channel)){ $this->_parseRSS($xml); } elseif(isset($xml->entry)){ $this->_parseAtom($xml); } } /** * RSS handling (http://en.wikipedia.org/wiki/RSS_%28file_format%29). * * @param object SimpleXML object (http://php.net/simplexml) * @return void */ private function _parseRSS($xml){ $nameSpaces = $xml->getDocNamespaces(TRUE); $descLimit = $this->_config['limits']['description']; $feedTitle = $xml->channel->title; $feedLink = $xml->channel->link; if(isset($xml->item)){ $feedEntries = $xml->item; } elseif(isset($xml->channel->item)){ $feedEntries = $xml->channel->item; } else{ $feedEntries = false; } if(is_object($feedEntries)){ $keyFormat = $this->_feedDataKeyFormat; foreach($feedEntries as $entry){ if(isset($entry->pubDate)){ $key = date($keyFormat, strtotime($entry->pubDate)); if(isset($this->_feedData[$key])){ $key = $this->_bogusDateKey(); } } elseif(isset($nameSpaces['dc'])){ // argh, custom namespaces kinda suck w/ simplexml... $entry->registerXPathNamespace('dc', $nameSpaces['dc']); $xpath = $entry->xpath('dc:date'); if(list( , $date) = each($xpath[0])){ $key = date($keyFormat, strtotime($date)); if(isset($this->_feedData[$key])){ $key = $this->_bogusDateKey(); } } } else{ $key = $this->_bogusDateKey(); } $data = array(); $data['feedTitle'] = strip_tags($feedTitle); $data['feedLink'] = strip_tags($feedLink); $data['itemTitle'] = strip_tags($entry->title); $data['itemLink'] = strip_tags($entry->link); $itemDesc = strip_tags(str_replace("\n", ' ', trim($entry->description))); if(strlen($itemDesc) > $descLimit){ $itemDesc = substr($itemDesc, 0, $descLimit).'...'; } $data['itemDesc'] = $itemDesc; $this->_feedData[$key] = $data; } } } /** * Atom handling (http://en.wikipedia.org/wiki/Atom_%28standard%29). * * @param object SimpleXML object (http://php.net/simplexml) * @return void */ private function _parseAtom($xml){ $descLimit = $this->_config['limits']['description']; $feedLinkAttributes = $xml->link->attributes(); $feedTitle = $xml->title; $feedLink = $feedLinkAttributes['href']; if(isset($xml->entry)){ $feedEntries = $xml->entry; } else{ $feedEntries = false; } if(is_object($feedEntries)){ $key = $this->_feedDataKeyFormat; foreach($feedEntries as $entry){ if(isset($entry->pubDate)){ $key = date($keyFormat, strtotime($entry->pubDate)); if(isset($this->_feedData[$key])){ $key = $this->_bogusDateKey(); } } else{ $key = $this->_bogusDateKey(); } $entryAttributes = $entry->link->attributes(); $data = array(); $data['feedTitle'] = strip_tags($feedTitle); $data['feedLink'] = strip_tags($feedLink); $data['itemTitle'] = strip_tags($entry->title); $data['itemLink'] = strip_tags($entryAttributes['href']); $itemDesc = strip_tags(str_replace("\n", ' ', trim($entry->content))); if(strlen($itemDesc) > $descLimit){ $itemDesc = substr($itemDesc, 0, $descLimit).'...'; } $data['itemDesc'] = $itemDesc; $this->_feedData[$key] = $data; } } } /** * Fetches the feed(s), populates and caches $this->_feedData if needed, or * acquires $this->_feedData from existing cache file. * * @param array Array keys in $this->_config['xmlSourceList'] * @return boolean True on success, false on error */ private function _getData($sourceChoices){ $returnVal = false; $secret = $this->_config['cache']['secret']; $scriptName = basename($_SERVER['PHP_SELF']); if($secret == ''){ print "

Sorry, but you have not defined a new secret key for secure cache filename generation.

\n" . "

Please be sure to set a random string for \$_config['cache']['secret'] in {$scriptName}.

\n"; } else{ $cacheDir = $this->_config['cache']['directory']; if(!function_exists('curl_init')){ print "

Sorry, but your PHP install is not configured .

\n" . "

Please create a writable directory at {$cacheDir}.

\n"; } elseif(!file_exists($cacheDir)){ print "

Sorry, but the cache directory cannot be found.

\n" . "

Please create a writable directory at {$cacheDir}.

\n"; } elseif(!is_dir($cacheDir)){ print "

Sorry, but the cache location is not a directory.

\n" . "

Please create a writable directory at {$cacheDir}.

\n"; } elseif(!is_writable($cacheDir)){ print "

Sorry, but the cache directory is not writable.

\n" . "

Please make the directory writable: {$cacheDir}.

\n"; } else{ $feedData = array(); $cacheFileName = sha1(serialize($sourceChoices).$secret); $cacheDestination = "{$cacheDir}/.{$cacheFileName}.cache"; if( ( ! file_exists($cacheDestination)) || (filemtime($cacheDestination) < strtotime($this->_config['cache']['timeToLive'].' ago')) ){ foreach($sourceChoices as $name){ $link = $this->_config['xmlSourceList'][$name]; $cleanName = htmlspecialchars($name); if(function_exists('curl_init')){ // try curl first $curlRes = curl_init($link); curl_setopt_array($curlRes, array( CURLOPT_RETURNTRANSFER => 1, CURLOPT_USERAGENT => $this->_userAgent )); if(($xmlSource = @curl_exec($curlRes)) === false){ print "

Sorry, but \"{$cleanName}\" cannot be reached at this time. (curl)

\n"; } } else{ // or use URL-aware fopen wrappers ini_set('user_agent', $this->_userAgent); if(($xmlSource = @file_get_contents($link)) === false){ print "

Sorry, but \"{$cleanName}\" cannot be reached at this time.

\n"; } } if( !isset($xmlSource) || (strlen($xmlSource) == 0) ){ print "

Nothing to parse for \"{$cleanName}\".

\n"; } elseif(($xml = @simplexml_load_string(trim($xmlSource))) === false){ print "

Sorry, but \"{$cleanName}\" cannot be parsed at this time.

\n"; } else{ $this->_parseFeed($xml); } } if(count($this->_feedData) == 0){ print "

Sorry, but every feed failed, and there was nothing to cache or display.

\n"; } else{ @file_put_contents($cacheDestination,serialize($this->_feedData)); $returnVal = true; } } else{ if(($cache = unserialize(@file_get_contents($cacheDestination))) === false){ print "

Sorry, but \"{$cacheFileName}\" cannot be unserialized at this time.

\n"; } else{ $this->_feedData = $cache; $this->_cacheMtime = filemtime($cacheDestination); $returnVal = true; } } } } return $returnVal; } /** * Main processing function. * * @return void */ private function _init(){ $xmlSourceList = $this->_config['xmlSourceList']; $cacheDir = $this->_config['cache']['directory']; $limitIncrement = $this->_config['limits']['increment']; $maxLimit = $this->_config['limits']['max']; $defaultLimit = $this->_config['limits']['default']; $defaultOrder = $this->_config['order']; $defaultOffset = 0; $cookieLifespan = strtotime('5 years'); $googleMobileUrl = 'http://www.google.com/gwt/n?_gwt_noimg=1&u='; // Get and clean user input $sentChoices = isset($_GET['src']) ? $_GET['src'] : array(); $limit = isset($_GET['lmt']) ? intval($_GET['lmt']) : $defaultLimit; $offset = isset($_GET['ofst']) ? intval($_GET['ofst']) : $defaultOffset; $order = isset($_GET['order']) ? $_GET['order'] : $defaultOrder; $useGoogle = intval($_GET['gmg']) > 0 ? true : false; $saveDefault = intval($_GET['sv']) > 0 ? true : false; if(!is_array($sentChoices)){ $sentChoices = array(); } if( (count($sentChoices) == 0) || (count($sentChoices) > count($xmlSourceList)) ){ // No or too many choices sent, get default (user | app) $sourceChoices = $this->_getDefaultSources(); } else{ $CleanSources = array(); foreach($sentChoices as $thisSource){ if(isset($xmlSourceList[$thisSource])){ // Add as safe source $CleanSources[] = $thisSource; } } if(count($CleanSources) == 0){ // All sent sources are invalid, get default (user | app) $sourceChoices = $this->_getDefaultSources(); } else{ // We have at leats one clean source, move on $sourceChoices = $CleanSources; if($saveDefault > 0){ // Save as a new user default if requested. setcookie( $this->_cookieDefaultSources, base64_encode(serialize($sourceChoices)), $cookieLifespan, '/' ); } } unset($CleanSources); } if( ( ! isset($_GET['lmt'])) || ($limit < $limitIncrement) || ($limit > $maxLimit) ){ // Invalid limit sent, get default (user | app) $limit = $this->_getDefaultLimit(); } elseif($saveDefault > 0){ // Save as a new user default if requested. setcookie($this->_cookieDefaultLimit, $limit, $cookieLifespan, '/'); } if( ($offset < 1) || ($offset > 100) ){ // Invalid offset sent, use default $offset = $defaultOffset; } if( ( ! isset($_GET['order'])) || ( ! isset($this->_orderTypes[$order]))){ // Invalid order sent, get default (user|app) $order = $this->_getDefaultOrder(); } elseif($saveDefault > 0){ // Save as a new user default if requested. setcookie($this->_cookieDefaultOrder, $order, $cookieLifespan, '/'); } if( ! isset($_GET['gmg'])){ // No Google Mobile prefs sent at all, get default (user|app) $useGoogle = $this->_getDefaultGoogle(); } elseif($saveDefault > 0){ // Save as a new user default if requested. setcookie( $this->_cookieDefaultGoogle, base64_encode(serialize($useGoogle)), $cookieLifespan, '/' ); } // Set page title and header $title = (count($sourceChoices) == 1) ? htmlspecialchars($sourceChoices[0]) : 'Mixed Sources'; // Print head block w/ defined and clean info print '<'.'?'.'xml version="1.0"?'.">\n" . "\n" . "\n" . " \n" . " {$this->_config['appName']} » {$title}\n" . " \n" . " \n" . " \n" . " \n" . "

{$title}

\n"; if($this->_getData($sourceChoices)){ $feedData = $this->_feedData; if($order == 'asc'){ ksort($feedData); } elseif($order == 'desc'){ krsort($feedData); } if(count($feedData) > 0){ print "
\n"; $i = 0; foreach($feedData as $date => $entry){ if($i >= $offset){ printf( '
%s
'."\n", ( $useGoogle ? $googleMobileUrl.rawurlencode($entry['itemLink']) : $entry['itemLink'] ), $entry['itemTitle'] ); printf( '
%s: %s
'."\n", ( $useGoogle ? $googleMobileUrl.rawurlencode($entry['feedLink']) : $entry['feedLink'] ), $entry['feedTitle'], $entry['itemDesc'] ); if(($i + 1) >= ($offset + $limit)) break; } $i++; } print "
\n"; } // Deal with the paging toolbar if(count($feedData) > $limit){ print "

| \n"; $link = $_SERVER['REQUEST_URI']; if(strstr($link, 'ofst=') === false) $link .= '?'; // "Previous" link if($offset > 0){ if(strstr($link, 'ofst=')) $link = preg_replace('/(\&ofst=\d{1,2})/','',$link); $link .= '&ofst='.($offset - $limit); print " « previous\n"; } // "Next" link if(count($feedData) > ($limit + $offset)){ if($offset > 0) print " | \n"; if(strstr($link, 'ofst=')) $link = preg_replace('/(\&ofst=\d{1,2})/','',$link); $link .= '&ofst='.($limit + $offset); print " next »\n"; } print " |

\n"; } // Print the cache specs $cacheMtime = $this->_cacheMtime; $parseMeLink = $this->_parseMeLink; if($useGoogle){ $parseMeLink = $googleMobileUrl.rawurlencode($parseMeLink); } print "

\n" . " Feed(s) cached on ".date('Y-m-d \a\t H:i:s T',$cacheMtime)."
\n" . " ".date('i \m\i\n. s \s\e\c.',( 1800 - (time() - $cacheMtime) ))." to refresh.
\n" . " Powered by parseMe\n" . "

\n" . "
\n" . "
\n" . "

\n" . "
\n"; foreach($xmlSourceList as $sourceName => $sourceURL){ $checkedSource = in_array($sourceName,$sourceChoices) ? 'checked="checked"' : ''; print " {$sourceName}
\n"; } // List the available items per page limits $checkedLimit = ($limit < $limitIncrement) ? $checkedLimit = 'checked="checked"' : ''; print "

\n" . "

\n" . "
\n" . " 1\n"; for($i=$limitIncrement; $i<=$maxLimit; $i+=$limitIncrement){ $checkedLimit = ($i == $limit) ? $checkedLimit = 'checked="checked"' : ''; print " {$i}\n"; if($i % ($limitIncrement * 2) != 0) print "
\n"; } // List the available sort options print "

\n" . "

\n" . "
\n"; foreach($this->_orderTypes as $orderType => $orderCaption){ $checkedOrder = ($orderType == $order) ? 'checked="checked"' : ''; print " {$orderCaption}
\n"; } // Opt in/out of using the Google Mobile Gateway $googleCheckedYes = $useGoogle ? 'checked="checked"' : ''; $googleCheckedNo = $useGoogle ? '' : 'checked="checked"'; print "

\n" . "

\n" . "
\n" . " Yes\n" . " No\n" . "
\n"; // Save options and end form print "

\n" . "

\n" . "
\n" . " Yes\n" . " No\n" . "

\n" . "

\n" . " \n" . "

\n" . "
\n" . "
\n"; } print " \n" . "\n"; } } ?>