Index: branches/rsr.v5.1.dev/web/punbb/extern.php
===================================================================
--- branches/rsr.v5.1.dev/web/punbb/extern.php	(revision 1)
+++ branches/rsr.v5.1.dev/web/punbb/extern.php	(revision 3)
@@ -1,282 +1,488 @@
 <?php
-/***********************************************************************
-
-  Copyright (C) 2002-2005  Rickard Andersson (rickard@punbb.org)
-
-  This file is part of PunBB.
-
-  PunBB is free software; you can redistribute it and/or modify it
-  under the terms of the GNU General Public License as published
-  by the Free Software Foundation; either version 2 of the License,
-  or (at your option) any later version.
-
-  PunBB is distributed in the hope that it will be useful, but
-  WITHOUT ANY WARRANTY; without even the implied warranty of
-  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-  GNU General Public License for more details.
-
-  You should have received a copy of the GNU General Public License
-  along with this program; if not, write to the Free Software
-  Foundation, Inc., 59 Temple Place, Suite 330, Boston,
-  MA  02111-1307  USA
-
-************************************************************************
-
+
+/**
+ * Copyright (C) 2008-2011 FluxBB
+ * based on code by Rickard Andersson copyright (C) 2002-2008 PunBB
+ * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher
+ */
+
+/*-----------------------------------------------------------------------------
 
   INSTRUCTIONS
 
-  Le script extern.php est utiliser pour inclure des informations Ã  
-  propos de vos forums sur des pages externes aux forums et pour 
-  syndiquer les discussions rÃ©centes via RSS. Le script peut afficher 
-  une liste de discussions rÃ©centes (triÃ©e par messages, dates ou 
-  derniers messages), une liste dÂutilisateurs actifs ou une 
-  collection de statistiques gÃ©nÃ©rales. Le script peut Ãªtre appeler 
-  directement par lÂintermÃ©diaire dÂune URL (pour RSS), de la 
-  commande inclue de PHP ou par lÂutilisation du Server Side 
-  Includes (SSI).
-
-  Le comportement du script est commandÃ© par lÂintermÃ©diaire de 
-  variables fournies au script dans lÂURL. Les diffÃ©rentes variables 
-  sont : action (que faut-il afficher), show (combien de discussions 
-  afficher), forum (lÂID du forum Ã  sonder pour rÃ©cupÃšrer les 
-  discussions) et type (sortie comme HTML ou RSS). La seule variable 
-  obligatoire est action. Les valeurs de possibles (par defaut) sont :
-
-    action: 
-	  active (affiche les discussions rÃ©cemment actives) (HTML ou RSS)
-      new (afficher les plus rÃ©centes discussions) (HTML ou RSS)
-      online (afficher les utilisateurs en ligne) (HTML)
-      online_full (idem, mais inclut une liste complÃšte) (HTML)
-      stats (afficher les statistiques des forums) (HTML)
-
-    show:   NÂimporte quÂelle valeur, nombre entier entre 1 et 50. 
-	        Cette variable est ignorÃ©es pour la sortie RSS. 15 par 
-			dÃ©faut.
-
-    fid:    Un ou plusieurs ID de forum (sÃ©parÃ©s par des virgules). 
-	        Si ignorÃ©e, des discussions de tous les forums lisibles 
-			par les invitÃ©s seront rÃ©cupÃ©rÃ©es.
-
-    nfid:   Un ou plusieurs ID de forum (sÃ©parÃ©s par des virgules) 
-	        qui seront ignorÃ©s. Ex. l'ID d'un forum de test.
-
-    type:   RSS. Toute autre chose signifie une sortie en HTML.
-
-
-  Voici quelques exemples en utilisant la fonction include() de PHP :
-
-    Afficher les 15 discussions les plus rÃ©cemment actives depuis tous les forums :
-    include('http://host.com/forums/extern.php?action=active');
-
-    Afficher les 10 discussions les plus rÃ©centes depuis les forums dÂID 5, 6 et 7 :
-    include('http://host.com/forums/extern.php?action=new&show=10&fid=5,6,7');
-
-    Afficher les utilisateurs en ligne :
-    include('http://host.com/forums/extern.php?action=online');
-
-    Afficher les utilisateurs en ligne avec une liste complÃšte :
-    include('http://host.com/forums/extern.php?action=online_full');
-
-    Afficher les statistiques des forums :
-    include('http://host.com/forums/extern.php?action=stats');
-
-
-  Voici quelques exemples en utilisant SSI :
-
-    Afficher les 5 discussions les plus rÃ©centes depuis les forums dÂID 11 et 22:
-    <!--#include virtual="forums/extern.php?action=new&show=5&fid=11,22" -->
-
-    Afficher les statistiques des forums :
-    <!--#include virtual="forums/extern.php?action=stats" -->
-
-
-  Et finalement quelques exemples en utilisant extern.php pour produire un fil RSS 0.91 :
-
-    Afficher les 15 discussions les plus rÃ©cemment actives :
-    http://host.com/extern.php?action=active&type=RSS
-
-    Afficher les 15 discussions les plus rÃ©cemment actives depuis le forum dÂID 2:
-    http://host.com/extern.php?action=active&type=RSS&fid=2
-
-  Ci-dessous vous trouverez des variables que vous pouvez modifier pour que le 
-  script se comporte selon vos besoins.
-
-/***********************************************************************/
-
-// Le nombre maximum de discussions qui seront affichÃ©es
-$show_max_topics = 60;
-
-// La longueur Ã  laquelle les sujets des discussions seront tronquÃ©es (pour HTML)
-$max_subject_length = 30;
-
-/***********************************************************************/
-
-// NE MODIFIEZ RIEN AU-DESSOUS DE CETTE LIGNE ! (Ã  moins que vous sachiez ce que vous faites)
-
-
-define('PUN_ROOT', './');
-@include PUN_ROOT.'config.php';
-
-// If PUN isn't defined, config.php is missing or corrupt
-if (!defined('PUN'))
-	exit('Le fichier "config.php" n\'existe pas ou est endommagÃ©. Veuillez lancer install.php pour installer PunBB.');
-
-
-// Make sure PHP reports all errors except E_NOTICE
-error_reporting(E_ALL ^ E_NOTICE);
-
-// Turn off magic_quotes_runtime
-set_magic_quotes_runtime(0);
-
-
-// Load the functions script
-require PUN_ROOT.'include/functions.php';
-
-// Load DB abstraction layer and try to connect
-require PUN_ROOT.'include/dblayer/common_db.php';
-
-// Load cached config
-@include PUN_ROOT.'cache/cache_config.php';
-if (!defined('PUN_CONFIG_LOADED'))
-{
-	require PUN_ROOT.'include/cache.php';
-	generate_config_cache();
-	require PUN_ROOT.'cache/cache_config.php';
-}
-
-// Make sure we (guests) have permission to read the forums
-$result = $db->query('SELECT g_read_board FROM '.$db->prefix.'groups WHERE g_id=3') or error('Unable to fetch group info', __FILE__, __LINE__, $db->error());
-if ($db->result($result) == '0')
-	exit('Vous n\'avez pas les permissions');
-
-
-// Attempt to load the common language file
-@include PUN_ROOT.'lang/'.$pun_config['o_default_lang'].'/common.php';
-if (!isset($lang_common))
-	exit('Il n\'y a pas de pack de langue \''.$pun_config['o_default_lang'].'\' d\'installÃ©. Veuillez rÃ©-installer une langue de ce nom.');
-
-if (!isset($_GET['action']))
-	exit('Aucun paramÃštre de fourni. Veuillez voir extern.php pour les instructions.');
-
-
-//
-// Converts the CDATA end sequence ]]> into ]]&gt;
-//
-function escape_cdata($str)
-{
-	return str_replace(']]>', ']]&gt;', $str);
-}
-
-
-//
+  This script is used to include information about your board from
+  pages outside the forums and to syndicate news about recent
+  discussions via RSS/Atom/XML. The script can display a list of
+  recent discussions, a list of active users or a collection of
+  general board statistics. The script can be called directly via
+  an URL, from a PHP include command or through the use of Server
+  Side Includes (SSI).
+
+  The scripts behaviour is controlled via variables supplied in the
+  URL to the script. The different variables are: action (what to
+  do), show (how many items to display), fid (the ID or IDs of
+  the forum(s) to poll for topics), nfid (the ID or IDs of forums
+  that should be excluded), tid (the ID of the topic from which to
+  display posts) and type (output as HTML or RSS). The only
+  mandatory variable is action. Possible/default values are:
+
+	action: feed - show most recent topics/posts (HTML or RSS)
+			online - show users online (HTML)
+			online_full - as above, but includes a full list (HTML)
+			stats - show board statistics (HTML)
+
+	type:   rss - output as RSS 2.0
+			atom - output as Atom 1.0
+			xml - output as XML
+			html - output as HTML (<li>'s)
+
+	fid:    One or more forum IDs (comma-separated). If ignored,
+			topics from all readable forums will be pulled.
+
+	nfid:   One or more forum IDs (comma-separated) that are to be
+			excluded. E.g. the ID of a a test forum.
+
+	tid:    A topic ID from which to show posts. If a tid is supplied,
+			fid and nfid are ignored.
+
+	show:   Any integer value between 1 and 50. The default is 15.
+
+	order:  last_post - show topics ordered by when they were last
+						posted in, giving information about the reply.
+			posted - show topics ordered by when they were first
+					 posted, giving information about the original post.
+
+-----------------------------------------------------------------------------*/
+
+define('PUN_QUIET_VISIT', 1);
+
+if (!defined('PUN_ROOT'))
+	define('PUN_ROOT', dirname(__FILE__).'/');
+require PUN_ROOT.'include/common.php';
+
+// The length at which topic subjects will be truncated (for HTML output)
+if (!defined('FORUM_EXTERN_MAX_SUBJECT_LENGTH'))
+	define('FORUM_EXTERN_MAX_SUBJECT_LENGTH', 30);
+
+// If we're a guest and we've sent a username/pass, we can try to authenticate using those details
+if ($pun_user['is_guest'] && isset($_SERVER['PHP_AUTH_USER']))
+	authenticate_user($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
+
+if ($pun_user['g_read_board'] == '0')
+{
+	http_authenticate_user();
+	exit($lang_common['No view']);
+}
+
+$action = isset($_GET['action']) ? strtolower($_GET['action']) : 'feed';
+
+// Handle a couple old formats, from FluxBB 1.2
+switch ($action)
+{
+	case 'active':
+		$action = 'feed';
+		$_GET['order'] = 'last_post';
+		break;
+
+	case 'new':
+		$action = 'feed';
+		$_GET['order'] = 'posted';
+		break;
+}
+
+//
+// Sends the proper headers for Basic HTTP Authentication
+//
+function http_authenticate_user()
+{
+	global $pun_config, $pun_user;
+
+	if (!$pun_user['is_guest'])
+		return;
+
+	header('WWW-Authenticate: Basic realm="'.$pun_config['o_board_title'].' External Syndication"');
+	header('HTTP/1.0 401 Unauthorized');
+}
+
+
+//
+// Output $feed as RSS 2.0
+//
+function output_rss($feed)
+{
+	global $lang_common, $pun_config;
+
+	// Send XML/no cache headers
+	header('Content-Type: application/xml; charset=utf-8');
+	header('Expires: '.gmdate('D, d M Y H:i:s').' GMT');
+	header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
+	header('Pragma: public');
+
+	echo '<?xml version="1.0" encoding="utf-8"?>'."\n";
+	echo '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">'."\n";
+	echo "\t".'<channel>'."\n";
+	echo "\t\t".'<atom:link href="'.pun_htmlspecialchars(get_current_url()).'" rel="self" type="application/rss+xml" />'."\n";
+	echo "\t\t".'<title><![CDATA['.escape_cdata($feed['title']).']]></title>'."\n";
+	echo "\t\t".'<link>'.pun_htmlspecialchars($feed['link']).'</link>'."\n";
+	echo "\t\t".'<description><![CDATA['.escape_cdata($feed['description']).']]></description>'."\n";
+	echo "\t\t".'<lastBuildDate>'.gmdate('r', count($feed['items']) ? $feed['items'][0]['pubdate'] : time()).'</lastBuildDate>'."\n";
+
+	if ($pun_config['o_show_version'] == '1')
+		echo "\t\t".'<generator>FluxBB '.$pun_config['o_cur_version'].'</generator>'."\n";
+	else
+		echo "\t\t".'<generator>FluxBB</generator>'."\n";
+
+	foreach ($feed['items'] as $item)
+	{
+		echo "\t\t".'<item>'."\n";
+		echo "\t\t\t".'<title><![CDATA['.escape_cdata($item['title']).']]></title>'."\n";
+		echo "\t\t\t".'<link>'.pun_htmlspecialchars($item['link']).'</link>'."\n";
+		echo "\t\t\t".'<description><![CDATA['.escape_cdata($item['description']).']]></description>'."\n";
+		echo "\t\t\t".'<author><![CDATA['.(isset($item['author']['email']) ? escape_cdata($item['author']['email']) : 'dummy@example.com').' ('.escape_cdata($item['author']['name']).')]]></author>'."\n";
+		echo "\t\t\t".'<pubDate>'.gmdate('r', $item['pubdate']).'</pubDate>'."\n";
+		echo "\t\t\t".'<guid>'.pun_htmlspecialchars($item['link']).'</guid>'."\n";
+
+		echo "\t\t".'</item>'."\n";
+	}
+
+	echo "\t".'</channel>'."\n";
+	echo '</rss>'."\n";
+}
+
+
+//
+// Output $feed as Atom 1.0
+//
+function output_atom($feed)
+{
+	global $lang_common, $pun_config;
+
+	// Send XML/no cache headers
+	header('Content-Type: application/atom+xml; charset=utf-8');
+	header('Expires: '.gmdate('D, d M Y H:i:s').' GMT');
+	header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
+	header('Pragma: public');
+
+	echo '<?xml version="1.0" encoding="utf-8"?>'."\n";
+	echo '<feed xmlns="http://www.w3.org/2005/Atom">'."\n";
+
+	echo "\t".'<title type="html"><![CDATA['.escape_cdata($feed['title']).']]></title>'."\n";
+	echo "\t".'<link rel="self" href="'.pun_htmlspecialchars(get_current_url()).'"/>'."\n";
+	echo "\t".'<link href="'.pun_htmlspecialchars($feed['link']).'"/>'."\n";
+	echo "\t".'<updated>'.gmdate('Y-m-d\TH:i:s\Z', count($feed['items']) ? $feed['items'][0]['pubdate'] : time()).'</updated>'."\n";
+
+	if ($pun_config['o_show_version'] == '1')
+		echo "\t".'<generator version="'.$pun_config['o_cur_version'].'">FluxBB</generator>'."\n";
+	else
+		echo "\t".'<generator>FluxBB</generator>'."\n";
+
+	echo "\t".'<id>'.pun_htmlspecialchars($feed['link']).'</id>'."\n";
+
+	$content_tag = ($feed['type'] == 'posts') ? 'content' : 'summary';
+
+	foreach ($feed['items'] as $item)
+	{
+		echo "\t".'<entry>'."\n";
+		echo "\t\t".'<title type="html"><![CDATA['.escape_cdata($item['title']).']]></title>'."\n";
+		echo "\t\t".'<link rel="alternate" href="'.pun_htmlspecialchars($item['link']).'"/>'."\n";
+		echo "\t\t".'<'.$content_tag.' type="html"><![CDATA['.escape_cdata($item['description']).']]></'.$content_tag.'>'."\n";
+		echo "\t\t".'<author>'."\n";
+		echo "\t\t\t".'<name><![CDATA['.escape_cdata($item['author']['name']).']]></name>'."\n";
+
+		if (isset($item['author']['email']))
+			echo "\t\t\t".'<email><![CDATA['.escape_cdata($item['author']['email']).']]></email>'."\n";
+
+		if (isset($item['author']['uri']))
+			echo "\t\t\t".'<uri>'.pun_htmlspecialchars($item['author']['uri']).'</uri>'."\n";
+
+		echo "\t\t".'</author>'."\n";
+		echo "\t\t".'<updated>'.gmdate('Y-m-d\TH:i:s\Z', $item['pubdate']).'</updated>'."\n";
+
+		echo "\t\t".'<id>'.pun_htmlspecialchars($item['link']).'</id>'."\n";
+		echo "\t".'</entry>'."\n";
+	}
+
+	echo '</feed>'."\n";
+}
+
+
+//
+// Output $feed as XML
+//
+function output_xml($feed)
+{
+	global $lang_common, $pun_config;
+
+	// Send XML/no cache headers
+	header('Content-Type: application/xml; charset=utf-8');
+	header('Expires: '.gmdate('D, d M Y H:i:s').' GMT');
+	header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
+	header('Pragma: public');
+
+	echo '<?xml version="1.0" encoding="utf-8"?>'."\n";
+	echo '<source>'."\n";
+	echo "\t".'<url>'.pun_htmlspecialchars($feed['link']).'</url>'."\n";
+
+	$forum_tag = ($feed['type'] == 'posts') ? 'post' : 'topic';
+
+	foreach ($feed['items'] as $item)
+	{
+		echo "\t".'<'.$forum_tag.' id="'.$item['id'].'">'."\n";
+
+		echo "\t\t".'<title><![CDATA['.escape_cdata($item['title']).']]></title>'."\n";
+		echo "\t\t".'<link>'.pun_htmlspecialchars($item['link']).'</link>'."\n";
+		echo "\t\t".'<content><![CDATA['.escape_cdata($item['description']).']]></content>'."\n";
+		echo "\t\t".'<author>'."\n";
+		echo "\t\t\t".'<name><![CDATA['.escape_cdata($item['author']['name']).']]></name>'."\n";
+
+		if (isset($item['author']['email']))
+			echo "\t\t\t".'<email><![CDATA['.escape_cdata($item['author']['email']).']]></email>'."\n";
+
+		if (isset($item['author']['uri']))
+			echo "\t\t\t".'<uri>'.pun_htmlspecialchars($item['author']['uri']).'</uri>'."\n";
+
+		echo "\t\t".'</author>'."\n";
+		echo "\t\t".'<posted>'.gmdate('r', $item['pubdate']).'</posted>'."\n";
+
+		echo "\t".'</'.$forum_tag.'>'."\n";
+	}
+
+	echo '</source>'."\n";
+}
+
+
+//
+// Output $feed as HTML (using <li> tags)
+//
+function output_html($feed)
+{
+
+	// Send the Content-type header in case the web server is setup to send something else
+	header('Content-type: text/html; charset=utf-8');
+	header('Expires: '.gmdate('D, d M Y H:i:s').' GMT');
+	header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
+	header('Pragma: public');
+
+	foreach ($feed['items'] as $item)
+	{
+		if (utf8_strlen($item['title']) > FORUM_EXTERN_MAX_SUBJECT_LENGTH)
+			$subject_truncated = pun_htmlspecialchars(pun_trim(utf8_substr($item['title'], 0, (FORUM_EXTERN_MAX_SUBJECT_LENGTH - 5)))).' âŠ';
+		else
+			$subject_truncated = pun_htmlspecialchars($item['title']);
+
+		echo '<li><a href="'.pun_htmlspecialchars($item['link']).'" title="'.pun_htmlspecialchars($item['title']).'">'.$subject_truncated.'</a></li>'."\n";
+	}
+}
+
 // Show recent discussions
-//
-if ($_GET['action'] == 'active' || $_GET['action'] == 'new')
-{
-	$order_by = ($_GET['action'] == 'active') ? 't.last_post' : 't.posted';
-	$forum_sql = '';
-
-	// Was any specific forum ID's supplied?
-	if (isset($_GET['fid']) && $_GET['fid'] != '')
-	{
-		$fids = explode(',', trim($_GET['fid']));
-		$fids = array_map('intval', $fids);
-
-		if (!empty($fids))
-			$forum_sql = ' AND f.id IN('.implode(',', $fids).')';
-	}
-
-	// Any forum ID's to exclude?
-	if (isset($_GET['nfid']) && $_GET['nfid'] != '')
-	{
-		$nfids = explode(',', trim($_GET['nfid']));
-		$nfids = array_map('intval', $nfids);
-
-		if (!empty($nfids))
-			$forum_sql = ' AND f.id NOT IN('.implode(',', $nfids).')';
-	}
-
-	// Should we output this as RSS?
-	if (isset($_GET['type']) && strtoupper($_GET['type']) == 'RSS')
-	{
-		$rss_description = ($_GET['action'] == 'active') ? $lang_common['RSS Desc Active'] : $lang_common['RSS Desc New'];
-		$url_action = ($_GET['action'] == 'active') ? '&amp;action=new' : '';
-
-		// Send XML/no cache headers
-		header('Content-Type: text/xml');
-		header('Expires: '.gmdate('D, d M Y H:i:s').' GMT');
-		header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
-		header('Pragma: public');
-
-		// It's time for some syndication!
-		echo '<?xml version="1.0" encoding="'.$lang_common['lang_encoding'].'"?>'."\r\n";
-		echo '<!DOCTYPE rss PUBLIC "-//Netscape Communications//DTD RSS 0.91//EN" "http://my.netscape.com/publish/formats/rss-0.91.dtd">'."\r\n";
-		echo '<rss version="0.91">'."\r\n";
-		echo '<channel>'."\r\n";
-		echo "\t".'<title>'.pun_htmlspecialchars($pun_config['o_board_title']).'</title>'."\r\n";
-		echo "\t".'<link>'.$pun_config['o_base_url'].'/</link>'."\r\n";
-		echo "\t".'<description>'.pun_htmlspecialchars($rss_description.' '.$pun_config['o_board_title']).'</description>'."\r\n";
-		echo "\t".'<language>en-us</language>'."\r\n";
-
-		// Fetch 15 topics
-		$result = $db->query('SELECT t.id, t.poster, t.subject, t.posted, t.last_post, f.id AS fid, f.forum_name FROM '.$db->prefix.'topics AS t INNER JOIN '.$db->prefix.'forums AS f ON f.id=t.forum_id LEFT JOIN '.$db->prefix.'forum_perms AS fp ON (fp.forum_id=f.id AND fp.group_id=3) WHERE (fp.read_forum IS NULL OR fp.read_forum=1) AND t.moved_to IS NULL'.$forum_sql.' ORDER BY '.$order_by.' DESC LIMIT 15') or error('Impossible de retrouver la liste des discussions', __FILE__, __LINE__, $db->error());
-
-		while ($cur_topic = $db->fetch_assoc($result))
-		{
-			if ($pun_config['o_censoring'] == '1')
-				$cur_topic['subject'] = censor_words($cur_topic['subject']);
-
-			echo "\t".'<item>'."\r\n";
-			echo "\t\t".'<title>'.pun_htmlspecialchars($cur_topic['subject']).'</title>'."\r\n";
-			echo "\t\t".'<link>'.$pun_config['o_base_url'].'/viewtopic.php?id='.$cur_topic['id'].$url_action.'</link>'."\r\n";
-			echo "\t\t".'<description><![CDATA['.escape_cdata($lang_common['Forum'].': <a href="'.$pun_config['o_base_url'].'/viewforum.php?id='.$cur_topic['fid'].'">'.$cur_topic['forum_name'].'</a><br />'."\r\n".$lang_common['Author'].': '.$cur_topic['poster'].'<br />'."\r\n".$lang_common['Posted'].': '.date('r', $cur_topic['posted']).'<br />'."\r\n".$lang_common['Last post'].': '.date('r', $cur_topic['last_post'])).']]></description>'."\r\n";
-			echo "\t".'</item>'."\r\n";
-		}
-
-		echo '</channel>'."\r\n";
-		echo '</rss>';
-	}
-
-
-	// Output regular HTML
+if ($action == 'feed')
+{
+	require PUN_ROOT.'include/parser.php';
+
+	// Determine what type of feed to output
+	$type = isset($_GET['type']) ? strtolower($_GET['type']) : 'html';
+	if (!in_array($type, array('html', 'rss', 'atom', 'xml')))
+		$type = 'html';
+
+	$show = isset($_GET['show']) ? intval($_GET['show']) : 15;
+	if ($show < 1 || $show > 50)
+		$show = 15;
+
+	// Was a topic ID supplied?
+	if (isset($_GET['tid']))
+	{
+		$tid = intval($_GET['tid']);
+
+		// Fetch topic subject
+		$result = $db->query('SELECT t.subject, t.first_post_id FROM '.$db->prefix.'topics AS t LEFT JOIN '.$db->prefix.'forum_perms AS fp ON (fp.forum_id=t.forum_id AND fp.group_id='.$pun_user['g_id'].') WHERE (fp.read_forum IS NULL OR fp.read_forum=1) AND t.moved_to IS NULL AND t.id='.$tid) or error('Unable to fetch topic info', __FILE__, __LINE__, $db->error());
+		if (!$db->num_rows($result))
+		{
+			http_authenticate_user();
+			exit($lang_common['Bad request']);
+		}
+
+		$cur_topic = $db->fetch_assoc($result);
+
+		if ($pun_config['o_censoring'] == '1')
+			$cur_topic['subject'] = censor_words($cur_topic['subject']);
+
+		// Setup the feed
+		$feed = array(
+			'title' 		=>	$pun_config['o_board_title'].$lang_common['Title separator'].$cur_topic['subject'],
+			'link'			=>	get_base_url(true).'/viewtopic.php?id='.$tid,
+			'description'		=>	sprintf($lang_common['RSS description topic'], $cur_topic['subject']),
+			'items'			=>	array(),
+			'type'			=>	'posts'
+		);
+
+		// Fetch $show posts
+		$result = $db->query('SELECT p.id, p.poster, p.message, p.hide_smilies, p.posted, p.poster_id, u.email_setting, u.email, p.poster_email FROM '.$db->prefix.'posts AS p INNER JOIN '.$db->prefix.'users AS u ON u.id=p.poster_id WHERE p.topic_id='.$tid.' ORDER BY p.posted DESC LIMIT '.$show) or error('Unable to fetch post info', __FILE__, __LINE__, $db->error());
+		while ($cur_post = $db->fetch_assoc($result))
+		{
+			$cur_post['message'] = parse_message($cur_post['message'], $cur_post['hide_smilies']);
+
+			$item = array(
+				'id'			=>	$cur_post['id'],
+				'title'			=>	$cur_topic['first_post_id'] == $cur_post['id'] ? $cur_topic['subject'] : $lang_common['RSS reply'].$cur_topic['subject'],
+				'link'			=>	get_base_url(true).'/viewtopic.php?pid='.$cur_post['id'].'#p'.$cur_post['id'],
+				'description'		=>	$cur_post['message'],
+				'author'		=>	array(
+					'name'	=> $cur_post['poster'],
+				),
+				'pubdate'		=>	$cur_post['posted']
+			);
+
+			if ($cur_post['poster_id'] > 1)
+			{
+				if ($cur_post['email_setting'] == '0' && !$pun_user['is_guest'])
+					$item['author']['email'] = $cur_post['email'];
+
+				$item['author']['uri'] = get_base_url(true).'/profile.php?id='.$cur_post['poster_id'];
+			}
+			else if ($cur_post['poster_email'] != '' && !$pun_user['is_guest'])
+				$item['author']['email'] = $cur_post['poster_email'];
+
+			$feed['items'][] = $item;
+		}
+
+		$output_func = 'output_'.$type;
+		$output_func($feed);
+	}
 	else
 	{
-		$show = isset($_GET['show']) ? intval($_GET['show']) : 15;
-		if ($show < 1 || $show > 50)
-			$show = 15;
-
-		// Fetch $show topics
-		$result = $db->query('SELECT t.id, t.subject FROM '.$db->prefix.'topics AS t INNER JOIN '.$db->prefix.'forums AS f ON f.id=t.forum_id LEFT JOIN '.$db->prefix.'forum_perms AS fp ON (fp.forum_id=f.id AND fp.group_id=3) WHERE (fp.read_forum IS NULL OR fp.read_forum=1) AND t.moved_to IS NULL'.$forum_sql.' ORDER BY '.$order_by.' DESC LIMIT '.$show) or error('Impossible de retrouver la liste des discussions', __FILE__, __LINE__, $db->error());
-
-		while ($cur_topic = $db->fetch_assoc($result))
-		{
-			if ($pun_config['o_censoring'] == '1')
-				$cur_topic['subject'] = censor_words($cur_topic['subject']);
-
-			if (pun_strlen($cur_topic['subject']) > $max_subject_length)
-				$subject_truncated = pun_htmlspecialchars(trim(substr($cur_topic['subject'], 0, ($max_subject_length-5)))).' ...';
-			else
-				$subject_truncated = pun_htmlspecialchars($cur_topic['subject']);
-
-			echo '<li><a href="'.$pun_config['o_base_url'].'/viewtopic.php?id='.$cur_topic['id'].'&amp;action=new" title="'.pun_htmlspecialchars($cur_topic['subject']).'">'.$subject_truncated.'</a></li>'."\n";
-		}
-	}
-
-	return;
-}
-
-
-//
+		$order_posted = isset($_GET['order']) && strtolower($_GET['order']) == 'posted';
+		$forum_name = '';
+		$forum_sql = '';
+
+		// Were any forum IDs supplied?
+		if (isset($_GET['fid']) && is_scalar($_GET['fid']) && $_GET['fid'] != '')
+		{
+			$fids = explode(',', pun_trim($_GET['fid']));
+			$fids = array_map('intval', $fids);
+
+			if (!empty($fids))
+				$forum_sql .= ' AND t.forum_id IN('.implode(',', $fids).')';
+
+			if (count($fids) == 1)
+			{
+				// Fetch forum name
+				$result = $db->query('SELECT f.forum_name FROM '.$db->prefix.'forums AS f LEFT JOIN '.$db->prefix.'forum_perms AS fp ON (fp.forum_id=f.id AND fp.group_id='.$pun_user['g_id'].') WHERE (fp.read_forum IS NULL OR fp.read_forum=1) AND f.id='.$fids[0]) or error('Unable to fetch forum name', __FILE__, __LINE__, $db->error());
+				if ($db->num_rows($result))
+					$forum_name = $lang_common['Title separator'].$db->result($result);
+			}
+		}
+
+		// Any forum IDs to exclude?
+		if (isset($_GET['nfid']) && is_scalar($_GET['nfid']) && $_GET['nfid'] != '')
+		{
+			$nfids = explode(',', pun_trim($_GET['nfid']));
+			$nfids = array_map('intval', $nfids);
+
+			if (!empty($nfids))
+				$forum_sql .= ' AND t.forum_id NOT IN('.implode(',', $nfids).')';
+		}
+
+		// Only attempt to cache if caching is enabled and we have all or a single forum
+		if ($pun_config['o_feed_ttl'] > 0 && ($forum_sql == '' || ($forum_name != '' && !isset($_GET['nfid']))))
+			$cache_id = 'feed'.sha1($pun_user['g_id'].'|'.$lang_common['lang_identifier'].'|'.($order_posted ? '1' : '0').($forum_name == '' ? '' : '|'.$fids[0]));
+
+		// Load cached feed
+		if (isset($cache_id) && file_exists(FORUM_CACHE_DIR.'cache_'.$cache_id.'.php'))
+			include FORUM_CACHE_DIR.'cache_'.$cache_id.'.php';
+
+		$now = time();
+		if (!isset($feed) || $cache_expire < $now)
+		{
+			// Setup the feed
+			$feed = array(
+				'title' 		=>	$pun_config['o_board_title'].$forum_name,
+				'link'			=>	'/index.php',
+				'description'	=>	sprintf($lang_common['RSS description'], $pun_config['o_board_title']),
+				'items'			=>	array(),
+				'type'			=>	'topics'
+			);
+
+			// Fetch $show topics
+			$result = $db->query('SELECT t.id, t.poster, t.subject, t.posted, t.last_post, t.last_poster, p.message, p.hide_smilies, u.email_setting, u.email, p.poster_id, p.poster_email FROM '.$db->prefix.'topics AS t INNER JOIN '.$db->prefix.'posts AS p ON p.id='.($order_posted ? 't.first_post_id' : 't.last_post_id').' INNER JOIN '.$db->prefix.'users AS u ON u.id=p.poster_id LEFT JOIN '.$db->prefix.'forum_perms AS fp ON (fp.forum_id=t.forum_id AND fp.group_id='.$pun_user['g_id'].') WHERE (fp.read_forum IS NULL OR fp.read_forum=1) AND t.moved_to IS NULL'.$forum_sql.' ORDER BY '.($order_posted ? 't.posted' : 't.last_post').' DESC LIMIT '.(isset($cache_id) ? 50 : $show)) or error('Unable to fetch topic info', __FILE__, __LINE__, $db->error());
+			while ($cur_topic = $db->fetch_assoc($result))
+			{
+				if ($pun_config['o_censoring'] == '1')
+					$cur_topic['subject'] = censor_words($cur_topic['subject']);
+
+				$cur_topic['message'] = parse_message($cur_topic['message'], $cur_topic['hide_smilies']);
+
+				$item = array(
+					'id'			=>	$cur_topic['id'],
+					'title'			=>	$cur_topic['subject'],
+					'link'			=>	'/viewtopic.php?id='.$cur_topic['id'].($order_posted ? '' : '&action=new'),
+					'description'	=>	$cur_topic['message'],
+					'author'		=>	array(
+						'name'	=> $order_posted ? $cur_topic['poster'] : $cur_topic['last_poster']
+					),
+					'pubdate'		=>	$order_posted ? $cur_topic['posted'] : $cur_topic['last_post']
+				);
+
+				if ($cur_topic['poster_id'] > 1)
+				{
+					if ($cur_topic['email_setting'] == '0' && !$pun_user['is_guest'])
+						$item['author']['email'] = $cur_topic['email'];
+
+					$item['author']['uri'] = '/profile.php?id='.$cur_topic['poster_id'];
+				}
+				else if ($cur_topic['poster_email'] != '' && !$pun_user['is_guest'])
+					$item['author']['email'] = $cur_topic['poster_email'];
+
+				$feed['items'][] = $item;
+			}
+
+			// Output feed as PHP code
+			if (isset($cache_id))
+			{
+				$fh = @fopen(FORUM_CACHE_DIR.'cache_'.$cache_id.'.php', 'wb');
+				if (!$fh)
+					error('Unable to write feed cache file to cache directory. Please make sure PHP has write access to the directory \''.pun_htmlspecialchars(FORUM_CACHE_DIR).'\'', __FILE__, __LINE__);
+
+				fwrite($fh, '<?php'."\n\n".'$feed = '.var_export($feed, true).';'."\n\n".'$cache_expire = '.($now + ($pun_config['o_feed_ttl'] * 60)).';'."\n\n".'?>');
+
+				fclose($fh);
+
+				if (function_exists('apc_delete_file'))
+					@apc_delete_file(FORUM_CACHE_DIR.'cache_'.$cache_id.'.php');
+			}
+		}
+
+		// If we only want to show a few items but due to caching we have too many
+		if (count($feed['items']) > $show)
+			$feed['items'] = array_slice($feed['items'], 0, $show);
+
+		// Prepend the current base URL onto some links. Done after caching to handle http/https correctly
+		$feed['link'] = get_base_url(true).$feed['link'];
+
+		foreach ($feed['items'] as $key => $item)
+		{
+			$feed['items'][$key]['link'] = get_base_url(true).$item['link'];
+
+			if (isset($item['author']['uri']))
+				$feed['items'][$key]['author']['uri'] = get_base_url(true).$item['author']['uri'];
+		}
+
+		$output_func = 'output_'.$type;
+		$output_func($feed);
+	}
+
+	exit;
+}
+
 // Show users online
-//
-else if ($_GET['action'] == 'online' || $_GET['action'] == 'online_full')
+else if ($action == 'online' || $action == 'online_full')
 {
 	// Load the index.php language file
 	require PUN_ROOT.'lang/'.$pun_config['o_default_lang'].'/index.php';
-	
+
 	// Fetch users online info and generate strings for output
 	$num_guests = $num_users = 0;
 	$users = array();
-	$result = $db->query('SELECT user_id, ident FROM '.$db->prefix.'online WHERE idle=0 ORDER BY ident', true) or error('Impossible de retrouver la liste des utilisateurs en ligne', __FILE__, __LINE__, $db->error());
+
+	$result = $db->query('SELECT user_id, ident FROM '.$db->prefix.'online WHERE idle=0 ORDER BY ident', true) or error('Unable to fetch online list', __FILE__, __LINE__, $db->error());
 
 	while ($pun_user_online = $db->fetch_assoc($result))
@@ -284,5 +490,5 @@
 		if ($pun_user_online['user_id'] > 1)
 		{
-			$users[] = '<a href="'.$pun_config['o_base_url'].'/profile.php?id='.$pun_user_online['user_id'].'">'.pun_htmlspecialchars($pun_user_online['ident']).'</a>';
+			$users[] = ($pun_user['g_view_users'] == '1') ? '<a href="'.pun_htmlspecialchars(get_base_url(true)).'/profile.php?id='.$pun_user_online['user_id'].'">'.pun_htmlspecialchars($pun_user_online['ident']).'</a>' : pun_htmlspecialchars($pun_user_online['ident']);
 			++$num_users;
 		}
@@ -291,19 +497,22 @@
 	}
 
-	echo $lang_index['Guests online'].': '.$num_guests.'<br />';
-
-	if ($_GET['action'] == 'online_full')
-		echo $lang_index['Users online'].': '.implode(', ', $users).'<br />';
+	// Send the Content-type header in case the web server is setup to send something else
+	header('Content-type: text/html; charset=utf-8');
+	header('Expires: '.gmdate('D, d M Y H:i:s').' GMT');
+	header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
+	header('Pragma: public');
+
+	echo sprintf($lang_index['Guests online'], forum_number_format($num_guests)).'<br />'."\n";
+
+	if ($action == 'online_full' && !empty($users))
+		echo sprintf($lang_index['Users online'], implode(', ', $users)).'<br />'."\n";
 	else
-		echo $lang_index['Users online'].': '.$num_users.'<br />';
-
-	return;
-}
-
-
-//
+		echo sprintf($lang_index['Users online'], forum_number_format($num_users)).'<br />'."\n";
+
+	exit;
+}
+
 // Show board statistics
-//
-else if ($_GET['action'] == 'stats')
+else if ($action == 'stats')
 {
 	// Load the index.php language file
@@ -311,22 +520,33 @@
 
 	// Collect some statistics from the database
-	$result = $db->query('SELECT COUNT(id)-1 FROM '.$db->prefix.'users') or error('Impossible de retrouver le nombre total d\'utilisateurs', __FILE__, __LINE__, $db->error());
-	$stats['total_users'] = $db->result($result);
-
-	$result = $db->query('SELECT id, username FROM '.$db->prefix.'users ORDER BY registered DESC LIMIT 1') or error('Impossible de retrouver le dernier utilisateur inscrit', __FILE__, __LINE__, $db->error());
-	$stats['last_user'] = $db->fetch_assoc($result);
-
-	$result = $db->query('SELECT SUM(num_topics), SUM(num_posts) FROM '.$db->prefix.'forums') or error('Impossible de retrouver le total de discussions et de messages', __FILE__, __LINE__, $db->error());
+	if (file_exists(FORUM_CACHE_DIR.'cache_users_info.php'))
+		include FORUM_CACHE_DIR.'cache_users_info.php';
+
+	if (!defined('PUN_USERS_INFO_LOADED'))
+	{
+		if (!defined('FORUM_CACHE_FUNCTIONS_LOADED'))
+			require PUN_ROOT.'include/cache.php';
+
+		generate_users_info_cache();
+		require FORUM_CACHE_DIR.'cache_users_info.php';
+	}
+
+	$result = $db->query('SELECT SUM(num_topics), SUM(num_posts) FROM '.$db->prefix.'forums') or error('Unable to fetch topic/post count', __FILE__, __LINE__, $db->error());
 	list($stats['total_topics'], $stats['total_posts']) = $db->fetch_row($result);
 
-	echo $lang_index['No of users'].': '.$stats['total_users'].'<br />';
-	echo $lang_index['Newest user'].': <a href="'.$pun_config['o_base_url'].'/profile.php?id='.$stats['last_user']['id'].'">'.pun_htmlspecialchars($stats['last_user']['username']).'</a><br />';
-	echo $lang_index['No of topics'].': '.$stats['total_topics'].'<br />';
-	echo $lang_index['No of posts'].': '.$stats['total_posts'];
-
-	return;
-}
-
-
-else
-	exit('Bad request');
+	// Send the Content-type header in case the web server is setup to send something else
+	header('Content-type: text/html; charset=utf-8');
+	header('Expires: '.gmdate('D, d M Y H:i:s').' GMT');
+	header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
+	header('Pragma: public');
+
+	echo sprintf($lang_index['No of users'], forum_number_format($stats['total_users'])).'<br />'."\n";
+	echo sprintf($lang_index['Newest user'], (($pun_user['g_view_users'] == '1') ? '<a href="'.pun_htmlspecialchars(get_base_url(true)).'/profile.php?id='.$stats['last_user']['id'].'">'.pun_htmlspecialchars($stats['last_user']['username']).'</a>' : pun_htmlspecialchars($stats['last_user']['username']))).'<br />'."\n";
+	echo sprintf($lang_index['No of topics'], forum_number_format($stats['total_topics'])).'<br />'."\n";
+	echo sprintf($lang_index['No of posts'], forum_number_format($stats['total_posts'])).'<br />'."\n";
+
+	exit;
+}
+
+// If we end up here, the script was called with some wacky parameters
+exit($lang_common['Bad request']);
