Add RSS Feeds
authorx2003bruneau <x2003bruneau@9869982d-c50d-0410-be91-f2a2ec7c7c7b>
Sun, 25 Feb 2007 16:48:39 +0000 (16:48 +0000)
committerFlorent Bruneau <florent.bruneau@polytechnique.org>
Fri, 4 Jan 2008 23:35:32 +0000 (00:35 +0100)
git-svn-id: svn+ssh://murphy/home/svn/banana/trunk@215 9869982d-c50d-0410-be91-f2a2ec7c7c7b

12 files changed:
Changelog
banana/banana.inc.php.in
banana/feed.inc.php [new file with mode: 0644]
banana/message.inc.php
banana/mimepart.inc.php
banana/page.inc.php
banana/spool.inc.php
banana/templates/banana-base.tpl
banana/templates/banana-boxlist.inc.tpl
banana/templates/banana-feed-rss2.tpl [new file with mode: 0644]
banana/templates/banana-thread.inc.tpl
img/feed.gif [new file with mode: 0644]

index 8ded7e9..399f0f1 100644 (file)
--- a/Changelog
+++ b/Changelog
@@ -1,3 +1,11 @@
+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
 
index 0670860..fde1931 100644 (file)
@@ -49,7 +49,7 @@ class Banana
     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;
@@ -77,6 +77,14 @@ class Banana
      */
     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
      */
@@ -99,6 +107,7 @@ class Banana
     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;
@@ -185,16 +194,26 @@ class Banana
         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;
@@ -249,6 +268,8 @@ class Banana
           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;
@@ -281,6 +302,16 @@ class Banana
         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()
@@ -324,6 +355,28 @@ class Banana
         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');
@@ -502,7 +555,7 @@ class Banana
             return false;
         }
         return true;
-    } 
+    }
 
     static public function createAllSpool(array $protos)
     {
@@ -519,7 +572,7 @@ class Banana
 
             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()) {
@@ -527,11 +580,45 @@ class Banana
                 }
                 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                                                      */
     /**************************************************************************/
diff --git a/banana/feed.inc.php b/banana/feed.inc.php
new file mode 100644 (file)
index 0000000..e4b3f90
--- /dev/null
@@ -0,0 +1,185 @@
+<?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:
+?>
index 66786da..02eeffc 100644 (file)
@@ -155,10 +155,7 @@ final class BananaMessage extends BananaMimePart
         $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)) {
@@ -172,6 +169,31 @@ final class BananaMessage extends BananaMimePart
         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));
index 1ca1ec8..e2fea25 100644 (file)
@@ -417,7 +417,7 @@ class BananaMimePart
 
     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="'
index dcdfb67..446621b 100644 (file)
@@ -141,6 +141,26 @@ class BananaPage extends Smarty
             $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);
@@ -152,6 +172,8 @@ class BananaPage extends Smarty
         $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'));
@@ -308,15 +330,18 @@ class BananaPage extends Smarty
      */
     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);
     }
 
@@ -352,7 +377,14 @@ function banana_trimwhitespace($source, &$smarty)
 }
 
 // }}}
+// {{{ function rss_date
+
+function rss_date($t)
+{
+    return date('r', $t);
+}
 
+// }}}
 
 // vim:set et sw=4 sts=4 ts=4 enc=utf-8:
 ?>
index ed898e2..b3c0bfc 100644 (file)
@@ -86,12 +86,12 @@ class BananaSpool
         $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);
@@ -114,15 +114,17 @@ class BananaSpool
         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;
index 26e6d7a..48d2387 100644 (file)
@@ -36,7 +36,7 @@
       <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}
index 921b5d3..102bf07 100644 (file)
     <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"}">
diff --git a/banana/templates/banana-feed-rss2.tpl b/banana/templates/banana-feed-rss2.tpl
new file mode 100644 (file)
index 0000000..b1543b4
--- /dev/null
@@ -0,0 +1,20 @@
+<?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: *}
index 9bf65fd..f8fe90b 100644 (file)
@@ -27,6 +27,7 @@
       {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}
diff --git a/img/feed.gif b/img/feed.gif
new file mode 100644 (file)
index 0000000..09ecd61
Binary files /dev/null and b/img/feed.gif differ