Stack2RSS 1.0
A simple web application that creates an RSS feed given an API route.
|
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 ?>