Location: PHPKode > scripts > ZebraFeeds > zebrafeeds/newsfeeds/includes/aggregator.php
<?php
// ZebraFeeds - copyright (c) 2006 Laurent Cazalet
// http://www.cazalet.org/zebrafeeds
//
// This program 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.
// This program 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.
//
//
// ZebraFeeds aggregator class

/* what does this class do?
this is the controller class (sort of)
- figure out where to get a list of channels from 
- aggregate those channels according to options: by channel, by date, trim...
  using a template
- display news from a single channel
- display content from a single news

this class shouldn't deal with refreshing feeds. it uses a single function 
to load a feed from a channel. 
how, how often this feed is refreshed is handled elsewhere.


list of channel object, some are real, some are made of

channel['type'] = 'real' or 'virtual'
if real : regular array
if virtual, has a list element, full of regular channel arrays
channel['list']


*/

if (!defined('ZF_VER')) exit;

require_once($zf_path . 'includes/feed.php');
require_once($zf_path . 'includes/view.php');
require_once($zf_path . 'includes/template.php');
require_once($zf_path . 'includes/history.php');
require_once($zf_path . 'includes/fetch.php');


class aggregator {

    // output template of this aggregation
    var $_template;

    // array of channels we have to get the feeds for
    // might come from an OPML list, or not
    var $channels;

    // if the aggregator use a list to get its channels from, 
    // here is the list. In this case $this->channels is a reference to
    // $list->channels
    var $list;

    var $errorLog;

    // feed options: only when viewmode is not feed
    var $_feedOptions;
    var $viewMode;

    // private properties
    // last RSS object got from magpie, the next to be 
    // aggregated
    var $_rss;

    /* array to help to track when the user came before */
    var $_visits; 

    // timestamp of start of processing
    var $_now;
    function aggregator() {
        $this->_feedOptions = array();
        $this->channels = array();
        $this->list = null;
        $this->_rss = null;
        $this->_template = null;
        $this->errorLog = '';
        $this->_viewMode = 'feed';
        $this->_feedOptions['trimtype'] = 'none';
        /* lastvisit= absolute last time seen here */
        $this->_visits['lastvisit'] = 0;
        /* lastsessionend= the time of end of previous session */
        $this->_visits['lastsessionend'] = 0;
        $this->_now = time();
    }

    function useList(&$subscriptionsList) {
        // record the information saying that this channel list
        // actually comes from a subscription list, not from 
        // a zf_addFeed call
        $this->list = &$subscriptionsList;
        $this->channels = &$subscriptionsList->channels;


        /* sets the default options from the list*/
        $this->_viewMode = $this->list->options['viewmode'];        

        //we set this only if we have to trim. otherwise we might be forcing an unwanted trimming
        if ($this->_viewMode == 'trim') {
            $this->_feedOptions['trimtype'] = $this->list->options['trimtype'];
            $this->_feedOptions['trimsize'] = $this->list->options['trimsize'];
        }
    }

    /* behavior settings */
    function setViewMode($mode) {
        $this->_viewMode = $mode;
    }


    function filterChannelPos($posString) {
        /* TODO: allow multiple positions */
        // so far, only one:

        $newchannels = array();
        for ($i=0; $i<count($this->channels); $i++) {
            if ($this->channels[$i]['position'] == $posString) {
                $newchannels[] = &$this->channels[$i];
            }
        }
        $this->channels = &$newchannels;

    }

    /* changes the trimming options. Also forces the view mode to trim */
    function setTrimOptions($trimtype, $trimsize) {
        $this->_viewMode = 'trim';
        $this->_feedOptions['trimsize'] = $trimsize;
        $this->_feedOptions['trimtype'] = $trimtype;
    }

    function matchNews($expression) {
        $this->_feedOptions['match'] = $expression;
    }

    function setUserFilterFunction($function) {
        if (function_exists($function)) {
            $this->_feedOptions['userfunction'] = $function;
        }
    }
    function useTemplate(&$template) {
        $this->_template = &$template;
    }

    /* main function, display the aggregator view 
       show and aggregated channels view, or a regular per-channel view
       according to viewmode and if matching a keyword has been requested
       meant to be for an HTML page. shows errors and credit if configured*/
    function viewPage() {
        if (count($this->channels) > 0) {
            $this->_template->printHeader();
            if (ZF_DEBUG==2) {
                zf_debug('Viewmode:'. $this->_viewMode);
                //print_r($this->_feedOptions);
            }


            // sort if not by feed or if we want to match a string
            if (($this->_viewMode != 'feed') || isset($this->_feedOptions['match'])) {
                $this->viewAggregatedChannels();
            } else {
                $this->viewChannels();
            }
            $this->_template->printFooter();
        } else {
            $this->displayStatus('No feeds');
        }
        // display errors at bottom. with a bit of JS trick, it could be at the top
        $this->displayErrors();
        $this->displayCredits();

    }

    /* renders a view showing news by channel. Traditional view (a la Yahoo) 
       channels of the channels list are rendered by position order
    */
    function viewChannels() {

        /*if we have feeds to display */
        foreach($this->channels as $i => $channel) {
            if ($channel['xmlurl'] != '') {
                // change the array key to be the position
                $sortedChannels[$channel['position']] = $channel;
            }
        }
        ksort($sortedChannels);
        //print_r($sortedChannels);

        foreach($sortedChannels as $channel) {
            if ($channel['issubscribed'] == "yes") {
                if (isset($channel['xmlurl']) && trim($channel['xmlurl']) != '' && $channel['showeditems'] > 0) {
                    if ($this->loadChannelFeed($channel)) {
                        $this->viewSingleChannel();
                        $this->_template->printBetween();
                    }
                }
            }
        }
    }


    /* renders a single real channel, the old way: channel header, 
    all items, natural order, only a max number of items
    optionally, we can render only the items, no header (this is for ajax calls)
    */
    function viewSingleChannel($viewAll = false, $onlyItems = false) {

        if (ZF_DEBUG==2) {
            zf_debug("viewing channel ".$this->_rss->channel['xmlurl']);
        }

        $feedOptions = array();
        if ($viewAll) {
            $feedOptions['trimtype'] = 'none';
        } else {
            $feedOptions['trimtype'] = 'news';
            $feedOptions['trimsize'] = $this->_rss->channel['showeditems'];            
        }

        $feedOptions['userfunction'] = $this->_feedOptions['userfunction'];
        if ($this->_rss != null) {
            $feed = new feed($feedOptions, $this->_rss);
            // false: we don't want sorting
            $feed->aggregate(false);
        } else {
            if (ZF_DEBUG==2) {
                zf_error('Internal error. no feed loaded.');
            }
        }
        if ($this->list != null) {
            $this->_template->addTags(array( 'list' => $this->list->name));
        }  else {
            $this->_template->addTags(array( 'list' => ''));
        } 
        
        $view = new view($this->_template, $feed);

        // could become true if we wanted date grouping for every channel
        $view->groupByDay = false;

        //render with no channel header if requested
        if ($onlyItems) {
            $view->renderNewsItems();
        } else {
            $view->render();
        }
    }

    /* AggregatedChannels: render a "virtual" channel
    create a feed aggregating all channels
    renders the feed by date */
    function viewAggregatedChannels() {

        $feed = &$this->makeFeed();
        if ($this->list != null) {
            //configure template to remove unhandled/unwanted buttons
            $this->_template->disableDisplayButtons();
            $this->_template->disableDynamicButtons();
            $this->_template->addTags(array( 'list' => $this->list->name));
        }  else {
            $this->_template->addTags(array( 'list' => ''));
        } 

        $this->_template->addTags(array( 'publisherurl' => ZF_HOMEURL)); 
        $view = new view($this->_template, $feed);
        $view->groupByDay = true;
        zf_debugRuntime("after aggregated view created");
        $view->render();
    }

    /* Display a simple export of aggregated channels
    create a feed aggregating all channels
    renders the feed by date. It's a much stripped down version
    of viewPage, that doesn't display error, nor credits
    doesn't set content type*/
    function exportAggregatedChannels() {

        $feed = &$this->makeFeed();
        $view = new view($this->_template, $feed);
        zf_debugRuntime("after aggregated view created");

        $this->_template->addTags(array('encoding' => ZF_ENCODING,
        'publisherurl' => ZF_HOMEURL ));
        $this->_template->printHeader();
        $view->render();
        $this->_template->printFooter();
    }




    /* prints a single news description, peeking from the last
    feed loaded. Does not use the template.
    Is called up on ajax requests
    $itemid : our own item id, generated internally
    
    returns the news formated with template section "extendedDescription"
  */
    function printItemContent($itemid) {

        /* force use of cache */

        if ( $this->_rss != null ) {
            if (ZF_DEBUG==2) {
                zf_debug("checking ".$this->_rss->channel['xmlurl']." for ".$itemid);
            }
            // let's lookup which item we want
            foreach ($this->_rss->items as $item) {
                // the news item
                $thisId = zf_makeId($this->_rss->channel['xmlurl'], $item['link']);
                // is it the one ?
                if (ZF_DEBUG==2) {
                    zf_debug('checking item with id '.$thisId);
                }
                if ( $thisId == $itemid ) {

                    if (ZF_DEBUG==2) {
                        zf_debug('Item Matches');
                    }
                    // HERE, we could/should use channel title/description

                    /* use the template if we are asked to */

                    $this->_template->printDynamicDescription($item);
                    return;
                }
            }
            echo  "Content not available for ".$this->_rss->channel['xmlurl']." (".$itemid.")";
        } 
    }


    /*Feed object factory: build the feed object, ready to be used by the view
    will load all RSS objects from the channels list and merge 
    them in a feed object, on which a reference is returned
    */
    function &makeFeed() {

        // use channel array for sources.

        //consistency check
        if ($this->_viewMode != 'trim') {
            $this->_feedOptions['trimtype'] = 'none';
        }
        // pass channel as a empty (null is not possible) variable, 
        //it'll trigger the right behavior in the feed constructor
        $feed = new feed($this->_feedOptions, $emptyvar);

        // if we are making a feed for a list, we have to initialize it's channel 
        // data structure, since it's not obtained from a real RSS feed.
        if ($this->list != null) {
            $this->initVirtualChannel( $feed->channel );
            $feed->listName = $this->list->name;
        }

        foreach($this->channels as $channel) {
            if ($channel['issubscribed'] == "yes") {
                if (isset($channel['xmlurl']) && trim($channel['xmlurl']) != '' ) {
                    if ($this->loadChannelFeed($channel) ) {
                        $feed->mergeWith($this->_rss);                            
                    }
                }
            }
        }
        $feed->aggregate();
        return $feed; 
    }


    /* fills the channel array of virtual channels/feeds */
    function initVirtualChannel(&$channel) {


        $channel['title'] = (ZF_OWNERNAME ==""?"":ZF_OWNERNAME." - ").$this->list->name;
        $channel['xmlurl'] = ZF_URL.'?f=rss&zflist='.urlencode($this->list->name);
        $channel['link'] = ZF_HOMEURL.'?zflist='.urlencode($this->list->name);
        $channel['id'] = md5($channel['xmlurl']);
        $channel['position'] = 0;
        $channel['isvirtual'] = true;
        $channel['last_fetched'] = 0;


        // fill the description
        $description = "Viewing ";
        if ($this->_feedOptions['trimtype'] != 'none') {
            $description .= 'last '.$this->_feedOptions['trimsize'].' '.(($this->_feedOptions['trimtype'] == 'news')?'news ':$this->_feedOptions['trimtype'].' of news ');
            $channel['link'] .= '&zftrim='.$this->_feedOptions['trimsize'].$this->_feedOptions['trimtype'];
        } else {
            $description .= "all news";
            $channel['link'] .= '&zfviewmode=date';
        }
        if (!empty($this->matchExpression) ){
            $description .= ' matching keyword \"'.$this->_feedOptions['match'].'\"';
        }
        $channel['description'] = $description;


    }



    /* Load the actual feed from a channel 
      (either from cache, or refresh from publisher)
     and make it available to other methods.
     wrapper around fetch_rss to enrich the rss array we get from magpieRSS 
     if successfull, the RSS object carries then all channel information

    return true if the feed was obtained, otherwise false*/
    function loadChannelFeed($channel, $refreshtime = 'default', $ignorehistory = false) {
        // TODO implement single global refreshtime

        // Refresh-time decision algorithm
        /* if provided $refreshtime is default
         *   if global option refresh mode == automatic,
                   use channel's refreshtime
              if global option refresh mode = on request 
                  force use of cache: set refreshtime to -1
           else
               use provided refreshtime
        */

        $usedrefreshtime = $refreshtime;
        if (!is_numeric($refreshtime) && $refreshtime == 'default') {
            if (ZF_DEBUG==2) {
                zf_debug("requesting default refresh time");
            }
            $usedrefreshtime = (ZF_REFRESHMODE == 'automatic')? $channel['refreshtime']: -1;
        } 

        if (ZF_DEBUG==2) {
            zf_debug("Refresh mode: ". ZF_REFRESHMODE." ; Refreshtime : $refreshtime ; used refresh time: $usedrefreshtime");
        }


        if (!$ignorehistory) {
            $channel['history'] = new history($channel['xmlurl']);
        }

        // QUICK DEBUG $refreshtime = -1;

        zf_debugRuntime("before fetch ".$channel['xmlurl']);
        $resultString = '';
        $this->_rss = zf_custom_fetch_rss( $channel, $usedrefreshtime, $resultString );
        zf_debugRuntime("after fetch ".$channel['xmlurl']);

        if ($this->_rss) {
            // add/replace data that magpie gave us
            // with our own configuration
            $this->_rss->channel['refreshtime'] = $channel['refreshtime'];
            $this->_rss->channel['showeditems'] = $channel['showeditems'];
            $this->_rss->channel['position'] = $channel['position'];

            // compare each item id with our fetch history, for this feed        
            // mark new items as such
            if ( !$ignorehistory) {
            
                $channel['history']->handleCurrentItems($this->_rss->items, 
                                                        $this->_visits['lastsessionend'], 
                                                        $this->_now);
                // delete unseen items from db
                $channel['history']->purge();
            }

            //print_r($this->_rss->items);


            return true;
        }

        // in case of Error
        // get error reason from zf_custom_fetch_rss

        if (ZF_DEBUG==2) {
            zf_error($resultString.' '. $channel['xmlurl']);
        }
        $this->_rss = null;
        // user friendly channel title 
        $channelTitle = isset($channel['title']) ? $channel['title']:$channel['xmlurl'];
        $this->errorLog .=$resultString.' feed from '. $channelTitle.'<br/>';
        return false;
    }

    /* generate bottom line */
    function displayCredits() {
        if ((!defined("ZF_SHOWCREDITS")) || (ZF_SHOWCREDITS!='no')) {
            echo ' <div id="generator">aggregated by <a href="http://www.cazalet.org/zebrafeeds">ZebraFeeds</a></div>';
        }

        zf_debugRuntime("after credits");
    }

    function displayStatus($message) {
        echo '<div class="zfchannelstatus">'.$message.'</div>';
    }

    function displayErrors() {
        if ((ZF_DISPLAYERROR =="yes")  && (!empty($this->errorLog)) ) {
            $this->displayStatus($this->errorLog);
        }
    }

    // fronts to the recordVisit method
    // as they are not called from the same place
    // whether set as client or server
    function recordServerVisit() {
        if (ZF_NEWITEMS=='server') {
            $this->_recordVisit();
        }
    }

    function recordClientVisit() {
        if (ZF_NEWITEMS=='client') {
            $this->_recordVisit();
        }
    }

    // store the current time
    function _recordVisit() {

        // 1: read visit information
        global $zf_path;


        $this->_visits['lastvisit'] = 0;
        $this->_visits['lastsessionend'] = 0;

        if (ZF_NEWITEMS=='server') {

            $name = $zf_path.ZF_DATADIR.'/visit.txt';

            $fp = @fopen($name, 'r');
            if ( ! $fp ) {
                if (ZF_DEBUG==7) {
                    zf_error("Failed to open visit file for reading: $name");
                }
            } else {
                if ($filesize = filesize($name) ) {
                    $data = fread( $fp, filesize($name) );
                    $this->_visits = unserialize( $data );
                }
                if (ZF_DEBUG == 7 ) {
                    zf_debug('last visit in server file: '.date('dS F Y h:i:s A', $this->_visits['lastvisit']));
                }
            }

        } else {

            // read visit time from cookie
            $this->_visits['lastvisit'] = $_COOKIE['lastvisit'];
            $this->_visits['lastsessionend'] = $_COOKIE['lastsessionend'];
            if (ZF_DEBUG == 7 ) {
                zf_debug('last visit in cookie: '.date('dS F Y h:i:s A', $this->_visits['lastvisit']));
                zf_debug('last session end in cookie: '.date('dS F Y h:i:s A', $this->_visits['lastsessionend']));
            }

        }

        // if our last visit happened X seconds ago
        if ($this->_now - $this->_visits['lastvisit'] > ZF_SESSION_DURATION) {
            $this->_visits['lastsessionend'] = $this->_visits['lastvisit'];
            if (ZF_DEBUG==7) {
                zf_debug("Session expired, last session end is now set to last visit");
            }

        }
        //echo date('dS F Y h:i:s A', $this->_now) . ' - '. date('dS F Y h:i:s A', $this->_visits['lastvisit']);
        $this->_visits['lastvisit'] = $this->_now;

        //STEP 2: record visit time
        if (ZF_NEWITEMS=='server') {
            // write visit information
            $fp = @fopen( $name, 'w' );

            if ( ! $fp ) {
                zf_error("History unable to open visit file for writing: $name");

            } else {
                $data = serialize( $this->_visits );
                fwrite( $fp, $data );
                fclose( $fp );
            }
        } else {
            // write visit info to cookie
            $expire = time()+ZF_VISITOR_COOKIE_EXPIRATION;
            $res1 = setcookie('lastvisit', $this->_visits['lastvisit'], $expire);
            // save last session end to cookie, but keep its value in our array
            /*if ($this->_visits['lastsessionend'] == 0) {
                $lastsessionend = $this->_visits['lastvisit'];
            } else {*/
                $lastsessionend = $this->_visits['lastsessionend'];
            /*}*/
            $res2 = setcookie('lastsessionend', $this->_visits['lastsessionend'], $expire);
            if (ZF_DEBUG == 7 ) {
                zf_debug('writing last visit in cookie: '.date('dS F Y h:i:s A', $this->_visits['lastvisit'])." ($res1)");
                zf_debug('writing last session end in cookie: '.date('dS F Y h:i:s A', $lastsessionend)." ($res2)");
            }

        }// ZF_NEWITEMS==server
    }

}



?>
Return current item: ZebraFeeds