+Sun, 25 Feb 2007 Florent Bruneau <florent.bruneau@m4x.org>
+
+ * RSS Feeds
+
+Wed, 21 Feb 2007 Florent Bruneau <florent.bruneau@m4x.org>
+
+ * MBox helper written in C
+
================================================================================
VERSION 1.5
static public $msgshow_mimeparts = array('multipart/report', 'multipart/mixed',
'text/html', 'text/plain', 'text/enriched', 'text', 'message');
static public $msgshow_xface = true;
- static public $msgshow_wrap = 78;
+ static public $msgshow_wrap = 80;
static public $msgshow_externalimages = false;
static public $msgshow_hasextimages = false;
static public $msgshow_withthread = true;
*/
static public $msgedit_mimeparts = array('multipart/report', 'multipart/mixed', 'text/plain', 'text/enriched', 'text/html', 'text', 'message');
+### Feed configuration ###
+ static public $feed_active = true;
+ static public $feed_format = 'rss2';
+ static public $feed_updateOnDemand = false; // Update the feed each time sbd check it
+ static public $feed_copyright = null;
+ static public $feed_namePrefix = 'Banana :: ';
+ static public $feed_size = 15; // Number of messages in the feed
+
### Protocole ###
/** News serveur to use
*/
const ACTION_BOX_NEEDED = 1; // mask
const ACTION_BOX_LIST = 2;
const ACTION_BOX_SUBS = 4;
+ const ACTION_BOX_FEED = 8;
const ACTION_MSG_LIST = 3;
const ACTION_MSG_READ = 5;
const ACTION_MSG_NEW = 9;
Banana::$first = isset($this->params['first']) ? $this->params['first'] : null;
Banana::$part = isset($this->params['part']) ? $this->params['part'] : 'text';
+ $action = @$this->params['action'];
+ if ($action == 'rss' || $action == 'rss2' || $action == 'atom') {
+ if ($action == 'rss') {
+ $action = 'rss2';
+ }
+ Banana::$feed_format = $action;
+ Banana::$action = Banana::ACTION_BOX_FEED;
+ return;
+ }
+
// Look for the action to execute
if (is_null(Banana::$group)) {
- if (isset($this->params['action']) && $this->params['action'] == 'subscribe') {
+ if ($action == 'subscribe') {
Banana::$action = Banana::ACTION_BOX_SUBS;
} else {
Banana::$action = Banana::ACTION_BOX_LIST;
}
return;
}
- $action = isset($this->params['action']) ? $this->params['action'] : null;
+
if (is_null(Banana::$artid)) {
if ($action == 'new') {
Banana::$action = Banana::ACTION_MSG_NEW;
case Banana::ACTION_BOX_LIST:
$error = $this->action_listBoxes();
break;
+ case Banana::ACTION_BOX_FEED:
+ return $this->action_feed(); // generate its own xml
case Banana::ACTION_MSG_LIST:
$error = $this->action_showThread(Banana::$group, Banana::$first);
break;
return Banana::$page->css;
}
+ /** Return the Link to the feed of the page
+ */
+ public function feed()
+ {
+ if (!Banana::$feed_active) {
+ return null;
+ }
+ return Banana::$page->makeURL(array('group' => Banana::$group, 'action' => Banana::$feed_format));
+ }
+
/** Return the execution backtrace of the current BananaProtocole
*/
public function backtrace()
return true;
}
+ protected function action_feed()
+ {
+ Banana::load('feed');
+ if (Banana::$group) {
+ $feed =& BananaFeed::getFeed();
+ return $feed->toXML();
+ }
+ if (Banana::$profile['subscribe']) {
+ $subfeed = null;
+ foreach (Banana::$profile['subscribe'] as $group) {
+ Banana::$group = $group;
+ if (Banana::$feed_updateOnDemand) {
+ $this->loadSpool($group);
+ }
+ $feed =& BananaFeed::getFeed();
+ $subfeed =& BananaFeed::merge($subfeed, $feed, _b_('Abonnements'), _b_('Mes abonnements Banana'));
+ }
+ return $subfeed->toXML();
+ }
+ return Banana::$page->feed();
+ }
+
protected function action_showThread($group, $first)
{
Banana::$page->setPage('thread');
return false;
}
return true;
- }
+ }
static public function createAllSpool(array $protos)
{
print "** $proto **\n";
foreach (array_keys($groups) as $g) {
- print "Generating spool for $g : ";
+ print "Generating spool for $g: ";
Banana::$group = $g;
$spool = $banana->loadSpool($g);
if (!$banana->checkErrors()) {
}
print "done.\n";
unset($spool);
+ Banana::$spool = null;
}
print "\n";
}
}
+ static public function refreshAllFeeds(array $protos)
+ {
+ Banana::load('feed');
+ Banana::$feed_updateOnDemand = true; // In order to force update
+ foreach ($protos as $proto) {
+ $banana = new Banana(array(), $proto);
+
+ if (!$banana->checkErrors()) {
+ continue;
+ }
+ $groups = Banana::$protocole->getBoxList();
+ if (!$banana->checkErrors()) {
+ continue;
+ }
+
+ print "** $proto **\n";
+ foreach (array_keys($groups) as $g) {
+ print "Generating feed cache for $g: ";
+ Banana::$group = $g;
+ $spool = $banana->loadSpool($g);
+ if (!$banana->checkErrors()) {
+ break;
+ }
+ $feed =& BananaFeed::getFeed();
+ print "done.\n";
+ unset($feed);
+ unset($spool);
+ Banana::$spool = null;
+ }
+ print "\n";
+ }
+ }
+
/**************************************************************************/
/* Private functions */
/**************************************************************************/
--- /dev/null
+<?php
+/********************************************************************************
+* banana/feed.inc.php : Feed Builder
+* ------------------------
+*
+* This file is part of the banana distribution
+* Copyright: See COPYING files that comes with this distribution
+********************************************************************************/
+
+require_once dirname(__FILE__) . '/banana.inc.php';
+
+define('BANANA_FEED_VERSION', '0.1');
+
+class BananaFeed
+{
+ /** Structure version
+ */
+ private $version;
+
+ /** Feed name
+ */
+ public $group;
+
+ /** Feed description
+ */
+ public $description;
+
+ /** A 'ordered by id' spool of the last messages
+ * Each Message is an array with :
+ * array('author' => author, 'date' => date (UNIX TS), 'title' => subject, 'body' => html body,
+ * 'link' => link, 'reply' => reply)
+ */
+ public $messages = array();
+
+ /** Create an empty feed
+ */
+ private function __construct()
+ {
+ $this->version = BANANA_FEED_VERSION;
+ $this->group = Banana::$group;
+ $this->description = Banana::$protocole->getDescription();
+ }
+
+ /** Update the feed, using current settings of Banana
+ * Calling this function suppose that Banana::$spool is the spool of the current box
+ */
+ public function update()
+ {
+ if (!Banana::$spool || Banana::$spool->group != $this->group) {
+ return false;
+ }
+ if (!Banana::$spool->ids) {
+ $spool_indexes = array();
+ } else {
+ $spool_indexes = Banana::$spool->ids;
+ sort($spool_indexes, SORT_NUMERIC);
+ $spool_indexes = array_slice($spool_indexes, -Banana::$feed_size, Banana::$feed_size);
+ }
+ $feed_indexes = array_keys($this->messages);
+ $old = array_diff($feed_indexes, $spool_indexes);
+ foreach ($old as $key) {
+ unset($this->messages[$key]);
+ }
+ $new = array_diff($spool_indexes, $feed_indexes);
+ foreach ($new as $key) {
+ $message =& Banana::$protocole->getMessage($key);
+ $array = array();
+ $array['author'] = $message->getAuthorName();
+ $array['date'] = $message->getHeaderValue('Date');
+ $array['title'] = $message->getHeaderValue('Subject');
+ $array['body'] = $message->toHtml();
+ $array['link'] = Banana::$page->makeUrl(array('group' => $this->group, 'artid' => $key));
+ if (Banana::$protocole->canSend()) {
+ $array['reply'] = Banana::$page->makeUrl(array('group' => $this->group, 'artid' => $key, 'action' => 'new'));
+ }
+ $this->messages[$key] = $array;
+ }
+ $this->writeToFile();
+ }
+
+ /** Get the spool corresponding with the current settings of Banana
+ */
+ static public function &getFeed()
+ {
+ $feed =& BananaFeed::readFromFile();
+ if (!$feed) {
+ $feed = new BananaFeed();
+ }
+ if (Banana::$feed_updateOnDemand) {
+ $feed->update();
+ }
+ return $feed;
+ }
+
+ /** Return the cache file name
+ */
+ static private function filename()
+ {
+ $file = Banana::$spool_root . '/' . Banana::$protocole->name() . '/';
+ if (!is_dir($file)) {
+ mkdir($file);
+ }
+ return $file . Banana::$protocole->filename() . '_feed';
+ }
+
+ /** Read a feed from a cache file
+ */
+ static private function &readFromFile()
+ {
+ $feed = null;
+ $file = BananaFeed::filename();
+ if (!file_exists($file)) {
+ return $feed;
+ }
+ $feed = unserialize(file_get_contents($file));
+ if ($feed->version != BANANA_FEED_VERSION) {
+ $feed = null;
+ }
+ return $feed;
+ }
+
+ /** Write a feed to a cache file
+ */
+ private function writeToFile()
+ {
+ $file = BananaFeed::filename();
+ file_put_contents($file, serialize($this));
+ }
+
+ /** Merge to feeds into a new one
+ */
+ static public function &merge(&$feed1, &$feed2, $name, $description = null)
+ {
+ if (!$feed1) {
+ $feed = null;
+ $feed1 =& $feed2;
+ $feed2 =& $feed;
+ }
+ if ($feed1->group == $name) {
+ $master =& $feed1;
+ $slave =& $feed2;
+ } else if ($feed2 && $feed2->group == $name) {
+ $master =& $feed2;
+ $slave =& $feed1;
+ } else {
+ $master = new BananaFeed();
+ $master->group = $name;
+ $master->description = $description;
+ foreach ($feed1->messages as $key=>$message) {
+ $message['title'] = '[' . $feed1->group . '] ' . $message['title'];
+ $master->messages[$feed1->group . '-' . $key] = $message;
+ }
+ $slave =& $feed2;
+ }
+ if (!$slave) {
+ return $master;
+ }
+ $messages = array();
+ $m1 = reset($master->messages);
+ $m2 = reset($slave->messages);
+ for ($i = 0 ; $i < 2 * Banana::$feed_size && ($m1 || $m2) ; $i++) {
+ if ($m2 && (!$m1 || $m1['date'] < $m2['date'])) {
+ $m2['title'] = '[' . $feed2->group . '] ' . $m2['title'];
+ $messages[$slave->group . '-' . key($slave->messages)] = $m2;
+ $m2 = next($slave->messages);
+ } else {
+ $messages[key($master->messages)] = $m1;
+ $m1 = next($master->messages);
+ }
+ }
+ $master->messages =& $messages;
+ return $master;
+ }
+
+ /** Generate the feed xml
+ */
+ public function toXML()
+ {
+ Banana::$page->assign_by_ref('feed', $this);
+ return Banana::$page->feed();
+ }
+}
+
+// vim:set et sw=4 sts=4 ts=4 enc=utf-8:
+?>
$mailto = '<a href="mailto:';
$result = banana_htmlentities($text);
- if (preg_match("/^([^ ]+@[^ ]+)$/", $text, $regs)) {
- $result = $mailto . $regs[1] . '">' . banana_htmlentities($regs[1]) . '</a>';
- }
- if (preg_match("/^<(.+@.+)>$/", $text, $regs)) {
+ if (preg_match("/^<?([^< ]+@[^> ]+)>?$/", $text, $regs)) {
$result = $mailto . $regs[1] . '">' . banana_htmlentities($regs[1]) . '</a>';
}
if (preg_match("/^([^ ]+@[^ ]+) \((.*)\)$/", $text, $regs)) {
return preg_replace("/\\\(\(|\))/","\\1",$result);
}
+ public function getAuthorName()
+ {
+ $text = $this->getHeaderValue('From');
+ $name = null;
+ if (preg_match("/^([^ ]+@[^ ]+) \((.*)\)$/", $text, $regs)) {
+ $name = $regs[2];
+ }
+ if (preg_match("/^\"?([^<>\"]+)\"? +<(.+@.+)>$/", $text, $regs)) {
+ $name = preg_replace("/^'(.*)'$/", '\1', $regs[1]);
+ $name = stripslashes($name);
+ }
+ if ($name) {
+ return preg_replace("/\\\(\(|\))/","\\1", $name);
+ }
+
+ if (function_exists('hook_getAuthorName') && $name = hook_getAuthorName($this)) {
+ return $name;
+ }
+
+ if (preg_match("/([^< ]+)@([^> ]+)/", $text, $regs)) {
+ return $regs[1];
+ }
+ return 'Anonymous';
+ }
+
static public function formatDate($text)
{
return strftime("%A %d %B %Y, %H:%M (fuseau serveur)", strtotime($text));
public function toHtml()
{
- list($type, $subtype) = $this->getType();
+ @list($type, $subtype) = $this->getType();
if ($type == 'image') {
$part = $this->id ? $this->id : $this->filename;
return '<img class="multipart" src="'
$this->page = null;
}
+ return $this->_run($tpl);
+ }
+
+ /** Generate feed XML code
+ */
+ public function feed()
+ {
+ @list($lg) = explode('_', Banana::$profile['locale']);
+ $tpl = 'banana-feed-' . Banana::$feed_format . '.tpl';
+ $this->assign('copyright', Banana::$feed_copyright);
+ $this->assign('title_prefix', Banana::$feed_namePrefix);
+ $this->assign('language', $lg);
+ $this->register_function('rss_date', 'rss_date');
+ return $this->_run($tpl);
+ }
+
+ /** Code generation
+ */
+ private function _run($tpl)
+ {
$this->assign('group', Banana::$group);
$this->assign('artid', Banana::$artid);
$this->assign('part', Banana::$part);
$this->assign('showboxlist', Banana::$spool_boxlist);
$this->assign('showthread', Banana::$msgshow_withthread);
$this->assign('withtabs' , Banana::$withtabs);
+ $this->assign('feed_format', Banana::$feed_format);
+ $this->assign('feed_active', Banana::$feed_active);
$this->register_function('url', array($this, 'makeUrl'));
$this->register_function('link', array($this, 'makeLink'));
*/
public function makeImgLink(array $params, &$smarty = null)
{
- $params['alt'] = _b_($params['alt']);
if (!isset($params['popup'])) {
- $params['popup'] = $params['alt'];
+ $params['popup'] = @$params['alt'];
}
$img = $this->makeImg($params, $smarty);
if (isset($params['text'])) {
$img .= ' ' . $params['text'];
}
$params['text'] = $img;
+ unset($params['alt']);
+ unset($params['img']);
+ unset($params['width']);
+ unset($params['height']);
return $this->makeLink($params, $smarty);
}
}
// }}}
+// {{{ function rss_date
+
+function rss_date($t)
+{
+ return date('r', $t);
+}
+// }}}
// vim:set et sw=4 sts=4 ts=4 enc=utf-8:
?>
$this->group = $group;
}
- public static function getSpool($group, $since = 0, $clean = false)
+ public static function &getSpool($group, $since = 0, $clean = false)
{
if (!is_null(Banana::$spool) && Banana::$spool->group == $group) {
$spool =& Banana::$spool;
} else {
- $spool = BananaSpool::readFromFile($group);
+ $spool =& BananaSpool::readFromFile($group);
}
if (is_null($spool)) {
$spool = new BananaSpool($group);
return $file . Banana::$protocole->filename();
}
- private static function readFromFile($group)
+ private static function &readFromFile($group)
{
+ $spool = null;
$file = BananaSpool::spoolFilename($group);
if (!file_exists($file)) {
- return null;
+ return $spool;
}
$spool = unserialize(file_get_contents($file));
if ($spool->version != BANANA_SPOOL_VERSION || $spool->mode != Banana::SPOOL_ALL) {
- return null;
+ $spool = null;
+ return $spool;
}
$spool->markAllAsRead();
return $spool;
<p class="center" style="padding: 0; margin: 0 0 1em 0">{$act.text}</p>
{/foreach}
{if $page eq 'forums'}
- {include file="banana-boxlist.inc.tpl" grouplist=$groups withstats=true}
+ {include file="banana-boxlist.inc.tpl" grouplist=$groups withstats=true withfeed=$feed_active}
{if $newgroups|@count}
<p>{"Les nouveaux groupes suivants ont été créés depuis votre dernière visite"|b}</p>
{include file="banana-boxlist.inc.tpl" grouplist=$newgroups withstats=true}
<th>{"Nouveaux"|b}</th>
{/if}
<th>{"Nom"|b}</th>
- <th>{"Description"|b}</th>
+ <th>
+ {if $withfeed}
+ <div class="action">
+ {imglink action=$feed_format img=feed alt="Flux"|b accesskey=f}
+ </div>
+ {/if}
+ {"Description"|b}
+ </th>
</tr>
{foreach from=$grouplist key=name item=grp}
<tr class="{cycle values="impair,pair"}">
--- /dev/null
+<?xml version="1.0"?>
+<rss version="2.0">
+ <channel>
+ <title>{$title_prefix}{$feed->group}</title>
+ <language>{$language}</language>
+ <link>{url group=$group}</link>
+ <description>{$feed->description}</description>
+ {foreach from=$feed->messages key=id item=message}
+ <item>
+ <title><![CDATA[{$message.title}]]></title>
+ <guid isPermaLink="false">{$id}</guid>
+ <link>{$message.link}</link>
+ <description><![CDATA[{$message.body}]]></description>
+ <author>{$message.author}</author>
+ <pubDate>{$message.date|rss_date}</pubDate>
+ </item>
+ {/foreach}
+ </channel>
+</rss>
+{* vim:set et sw=2 sts=2 ts=2 enc=utf-8: *}
{if $protocole->canSend()}
<div class="action">
{imglink group=$group action=new img=post alt="Nouveau message"|b accesskey=p}
+ {if $feed_active}{imglink group=$group action=$feed_format img=feed alt="Flux"|b accesskey=f}{/if}
</div>
{/if}
{"Auteur"|b}