Stack2RSS 1.0
A simple web application that creates an RSS feed given an API route.

src/feed.php

Go to the documentation of this file.
00001 <?php
00002 
00003 /** \mainpage Stack2RSS Documentation
00004  *
00005  * \section introduction Introduction
00006  * Stack2RSS is a simple web application that creates an RSS feed given an
00007  * API route.
00008  *
00009  * \section usage Usage
00010  * Using Stack2RSS is relatively simple. The general format of the URL for a feed looks something like this:
00011  * <pre>/ <b>{Stack Exchange Site Domain}</b> / <b>{API route}</b> ? <b>{Parameters}</b></pre>
00012  * There are a couple things to note here:
00013  * - The domain is of the form <kbd>site.com</kbd> (examples: <kbd>stackoverflow.com</kbd>, <kbd>apple.stackexchange.com</kbd>, <kbd>stackauth.com</kbd>).
00014  * - You do not need to specify an API key as Stack2RSS will use its own key.
00015  * - You can use v1.0 methods by passing the 'api_version=1.0' query string parameter.
00016  *
00017  * \section summary Summary of the Process
00018  * -# Fetch and process all parameters
00019  * -# Make API request to given method
00020  * -# Convert results returned to HTML
00021  * -# Create RSS-compliant feed from items
00022  * -# Echo it to the client
00023  *  
00024  * \section structure Code Structure
00025  * The code is basically divided into two sections:
00026  * - the Stack2RSS class that is actually responsible for the requests / parameter parsing / etc.
00027  * - a bunch of classes that derive from BaseConverter which convert one particular type of item (such as 'user' or 'question') into an HTML representation
00028  *
00029  * \section imp_classes Important Classes
00030  * - Stack2RSS
00031  * - Converter
00032  *
00033  * \section conv_classes Conversion Classes
00034  * - AnswerConverter
00035  * - APISiteConverter
00036  * - AssociatedUserConverter
00037  * - BadgeConverter
00038  * - CommentConverter
00039  * - ItemConverter
00040  * - PostTimelineConverter
00041  * - PrivilegeConverter
00042  * - QuestionConverter
00043  * - RepChangeConverter
00044  * - RevisionConverter
00045  * - StatisticsConverter
00046  * - TagSynonymConverter
00047  * - TagWikiConverter
00048  * - TagConverter
00049  * - TopTagConverter
00050  * - TopUserConverter
00051  * - UserTimelineConverter
00052  * - UserConverter
00053  */
00054  
00055 /// \file feed.php Contains the main class, Stack2RSS.
00056 
00057 // Load our configuration file
00058 require_once 'config.inc';
00059 
00060 // Include the different types of conversion classes
00061 require_once 'converters/answer_converter.php';
00062 require_once 'converters/api_site_converter.php';
00063 require_once 'converters/associated_user_converter.php';
00064 require_once 'converters/badge_converter.php';
00065 require_once 'converters/comment_converter.php';
00066 require_once 'converters/item_converter.php';
00067 require_once 'converters/post_timeline_converter.php';
00068 require_once 'converters/privilege_converter.php';
00069 require_once 'converters/question_converter.php';
00070 require_once 'converters/rep_change_converter.php';
00071 require_once 'converters/revision_converter.php';
00072 require_once 'converters/statistics_converter.php';
00073 require_once 'converters/tag_synonym_converter.php';
00074 require_once 'converters/tag_wiki_converter.php';
00075 require_once 'converters/tag_converter.php';
00076 require_once 'converters/top_tag_converter.php';
00077 require_once 'converters/top_user_converter.php';
00078 require_once 'converters/user_timeline_converter.php';
00079 require_once 'converters/user_converter.php';
00080 
00081 /// Fetches, parses, and processes API requests
00082 /**
00083  * Stack2RSS is responsible for parsing the parameters it is passed, fetching the API requests, and finally processing them by creating an instance of the appropriate conversion class and passing along the data.
00084  */
00085 class Stack2RSS
00086 {
00087     /// Returns an error to the client and exits
00088     /**
00089      * \param[in] $error_str a string representation of the error
00090      */
00091     private function OutputError($error_str, $status_line='404 Not Found')
00092     {
00093         // Outputting an error that consists of a 404 status code
00094         // followed by sending the message
00095         header("HTTP/1.1 $status_line");
00096         
00097         $error_title = 'There was an error processing this page.';
00098         $error_desc  = $error_str;
00099         require 'error.php';
00100         
00101         // Now simply end program execution
00102         exit;
00103     }
00104     
00105     /// Fetches the raw JSON data from the API for the given URL
00106     /**
00107      * <i>Note: this method uses PHP's cURL methods.</i>
00108      * \param[in] $url the URL of the API method to query
00109      * \return an array object with the JSON-decoded data
00110      */
00111     private function FetchData($url)
00112     {
00113         $ch = curl_init();
00114         
00115         curl_setopt($ch,CURLOPT_URL,$url);
00116         curl_setopt($ch,CURLOPT_HEADER,0);
00117         curl_setopt($ch,CURLOPT_USERAGENT,"Stack2RSS/1.0");
00118         curl_setopt($ch,CURLOPT_ENCODING,'gzip');  // Needed by API
00119         curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);
00120         
00121         $data = curl_exec($ch);
00122         
00123         // Check for an error
00124         if($data === FALSE)
00125             $this->OutputError('There was an error retrieving the data from the API servers.', '500 Internal Server Error');
00126         
00127         curl_close($ch);
00128         
00129         // Now decode the data and check for an API error.
00130         $decoded_data = json_decode($data, TRUE);
00131         
00132         if(isset($decoded_data['error']))
00133             $this->OutputError("Error reported by the API:<br /><br /><kbd>{$decoded_data['error']['message']}</kbd>");
00134         
00135         return $decoded_data;
00136     }
00137     
00138     /// Gathers information from parameters and generates a URL
00139     /**
00140      * This method is responsible for finding all of the parameters that have been passed to the script including the site and constructing the URL that will be passed to FetchData().
00141      * \return the generated URL
00142      */
00143     private function GenerateURLFromParameters()
00144     {
00145         $site    = 'stackoverflow.com'; // use SO by default
00146         $method  = 'questions';         // '/questions' method by default
00147         $version = '1.1';               // the API version to use
00148 
00149         // This array contains all of the parameters that will be
00150         // included in the API request. We always include our API key
00151         // at a minimum.
00152         $parameters = array('key=Gbn011-pzEi4tPK3BwbYIg');
00153         
00154         // Loop through all of the $_GET variables.
00155         foreach($_GET as $key => $value)
00156         {
00157             // All of the parameters are added to the list except for two
00158             // special parameters: site and method
00159             if($key == 'site')
00160                 $site = $value;
00161             elseif($key == 'method')
00162                 $method = $value;
00163             elseif($key == 'api_version')
00164                 $version = $value;
00165             else
00166                 $parameters[] = urlencode($key) . '=' . urlencode($value);
00167         }
00168         
00169         // If no method was supplied, we redirect the user to the home
00170         // page where they can discover why it makes no sense to do such a thing.
00171         if($method == '')
00172         {
00173             // Output an error
00174             require 'error.php';
00175             exit;
00176         }
00177         
00178         // Make sure the site has a TLD domain in it (for backward compatibility,
00179         // we insert '.com' if there isn't one. Also modify the $_GET variable to include it.
00180         if(substr($site, -4) != '.com')
00181         {
00182             $site .= '.com';
00183             $_GET['site'] .= '.com';
00184         }
00185         
00186         // Unless the site is stackauth, we prepend 'api.' to the URL
00187         $prepend = ($site == 'stackauth.com')?'':'api.';
00188         
00189         // Now construct the URL to return
00190         return "http://{$prepend}{$site}/{$version}/{$method}?" . implode('&', $parameters);
00191     }
00192     
00193     /// Determines the correct conversion class to instantiate and uses it
00194     /**
00195      * \param[in] $json_data the JSON-encoded data returned by the API
00196      * \return an array of items with HTML for title, description, etc.
00197      */
00198     private function RunConversion($json_data)
00199     {
00200         // Find the array item in the response
00201         foreach($json_data as $key => $value)
00202             if(is_array($value))
00203             {
00204                 // Now check to see what type of key was used
00205                 switch($key)
00206                 {
00207                     case 'answers':
00208                         $converter = new AnswerConverter();
00209                         break;
00210                     case 'api_sites':
00211                         $converter = new APISiteConverter();
00212                         break;
00213                     case 'associated_users':
00214                         $converter = new AssociatedUserConverter();
00215                         break;
00216                     case 'badges':
00217                         $converter = new BadgeConverter();
00218                         break;
00219                     case 'comments':
00220                         $converter = new CommentConverter();
00221                         break;
00222                     case 'items':
00223                         $converter = new ItemConverter();
00224                         break;
00225                     case 'post_timelines':
00226                         $converter = new PostTimelineConverter();
00227                         break;
00228                     case 'privileges':
00229                         $converter = new PrivilegeConverter();
00230                         break;
00231                     case 'questions':
00232                         $converter = new QuestionConverter();
00233                         break;
00234                     case 'rep_changes':
00235                         $converter = new RepChangeConverter();
00236                         break;
00237                     case 'revisions':
00238                         $converter = new RevisionConverter();
00239                         break;
00240                     case 'statistics':
00241                         $converter = new StatisticsConverter();
00242                         break;
00243                     case 'tag_synonyms':
00244                         $converter = new TagSynonymConverter();
00245                         break;
00246                     case 'tag_wikis':
00247                         $converter = new TagWikiConverter();
00248                         break;
00249                     case 'tags':
00250                         $converter = new TagConverter();
00251                         break;
00252                     case 'top_tags':
00253                         $converter = new TopTagConverter();
00254                         break;
00255                     case 'top_users':
00256                         $converter = new TopUserConverter();
00257                         break;
00258                     case 'user_timelines':
00259                         $converter = new UserTimelineConverter();
00260                         break;
00261                     case 'users':
00262                         $converter = new UserConverter();
00263                         break;
00264                     default:
00265                         // The response is a type we do not recognize
00266                         $this->OutputError('Unknown data type returned by the API.', '500 Internal Server Error');
00267                         break;
00268                 }
00269                 
00270                 // Perform the actual conversion and return the results
00271                 return $converter->PerformConversion($value);
00272             }
00273         
00274         // If we reach this line, then no array object was found
00275         $this->OutputError('The API response did not contain an array.', '500 Internal Server Error');
00276     }
00277     
00278     /// Processes the feed
00279     /**
00280      * <i>Note: this is the only public function in this class - it basically just wraps the other private functions, tying them all together.</i>
00281      */
00282     public function ProcessFeed()
00283     {
00284         $url        = $this->GenerateURLFromParameters();
00285         $json_data  = $this->FetchData($url);
00286         $html_items = $this->RunConversion($json_data);
00287         
00288         // Now output the results
00289         header('Content-type: application/rss+xml');
00290         echo $this->CreateFeedData($html_items, $url);
00291     }
00292     
00293     /// Creates the actual RSS feed data
00294     /**
00295      * This function returns the XML for the RSS feed including all of the metadata for the feed.
00296      * \param[in] $items_data an array that contians the data (title, HTML) for each item
00297      * \param[in] $feed_url   the URL for the API request
00298      * \return the actual feed data itself
00299      */
00300     private function CreateFeedData($items_data, $feed_url)
00301     {
00302         // Gather the data we need for creating the XML
00303         $encoded_feed_url = htmlentities($feed_url);
00304         $build_date       = date(DATE_RSS);
00305         
00306         // Now create the XML that combines the feed items together
00307         $feed_item_html = '';
00308         foreach($items_data as $item_data)
00309         {
00310 $feed_item_html .= <<<EOD
00311     <item>
00312       <title><![CDATA[{$item_data['title']}]]></title>
00313       <link>{$item_data['link']}</link>
00314       <description><![CDATA[{$item_data['description']}]]></description>
00315       <pubDate>{$item_data['date']}</pubDate>
00316     </item>
00317 EOD;
00318         }
00319         
00320         // Now return that data
00321 return <<<EOD
00322 <?xml version="1.0" encoding="UTF-8" ?>
00323 <rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
00324   <channel>
00325     <title>Stack2RSS Custom Feed</title>
00326     <link>{$encoded_feed_url}</link>
00327     <description>This feed contains the result of the API URL {$encoded_feed_url}.</description>
00328     <language>en</language>
00329     <generator>PHP 5.2</generator>
00330     <lastBuildDate>{$build_date}</lastBuildDate>
00331 {$feed_item_html}
00332   </channel>
00333 </rss>
00334 EOD;
00335     }
00336 }
00337 
00338 // Create an instance of Stack2RSS.
00339 $feed = new Stack2RSS();
00340 $feed->ProcessFeed();
00341 
00342 ?>
 All Classes Files Functions Enumerations