<?php
// txt2rss.php -- Glob text files into an RSS 1.0 XML file.

// Copyright (C) 2005, 2006 Aaron Hawley <ashawley@gnu.uvm.edu>

// 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., 51 Franklin Street, Fifth Floor, Boston, MA
// 02110-1301, USA.

// http://agave.garden.org/~aaronh/php/txt2rss.html

// $Id: txt2rss.php,v 1.3 2007-01-10 14:12:12-05 ashawley Exp $

/** RssLib
* Some auxillary RSS 1.0 functions.
*/
class RssLib
{
    var
$date_format = "Y-m-d\\Th:i:sO";
    function
setDateFormat($fmt)
    {
        
$this->date_format;
    }
// end func setDateFormat

    
function formatDate($date)
    {
        return
$this->w3cDateTimeFormat(date($this->date_format, $date));
    }
// end func formatDate

    // http://www.w3.org/TR/NOTE-datetime
    // Profile of ISO 8601
    // http://www.iso.ch/iso/en/prods-services/popstds/datesandtime.html
    
function w3cDateTimeFormat($date)
    {
        return
preg_replace('/\d\d$/', ':\0', $date);
    }
// end func w3cDateTimeFormat
    
} // end class RssLib

/** TextFileToRssItem
* Takes a text file RSS item data and can give the corresponding XML.
*/
class TextFileToRssItem extends RssLib
{
    var
$rss_lines = array('url' => "http://",
                           
'title' => "No Title",
                           
'description' => "No description",
                           
'author' => "No Authors");

    function
getArray($file)
    {
        if (!
file_exists($file)) {
            return new
Error("File does not exist", $file);
        }
        if (!
is_readable($file)) {
            return new
Error("File is not readable", $file);
        }
        
$date = filemtime($file);
        
$txt_file = new TextLineFile($this->rss_lines);
        
$data = $txt_file->parseFile($file);
        if (
Main::instance_of($data, 'Error')) {
            return
$data;
        }
        
$data['file'] = $file;
        
$data['date'] = $this->formatDate($date);
        return
$data;
    }
// end func getArray

    
function getItem($file)
    {
        
$id = $this->getArray($file);
        if (
Main::instance_of($id, 'Error')) {
            return
$id;
        }
        
$xml = '<item rdf:about="' . htmlspecialchars($id['url']) . '">'
        
. "\n" . '  <!-- file: ' . htmlspecialchars($id['file']) . ' -->'
        
. "\n" . '  <title>' . htmlspecialchars($id['title']) . '</title>'
        
. "\n" . '  <link>' . htmlspecialchars($id['url']) . '</link>'
        
. "\n" . '  <description>' . htmlspecialchars($id['description'])
               .
'</description>'
        
. "\n" . '  <dc:creator>' . htmlspecialchars($id['author'])
               .
'</dc:creator>'
        
. "\n" . '  <dc:date>' . htmlspecialchars($id['date'])
               .
'</dc:date>'
        
. "\n" . '</item>';
        return array(
$id['url'] => $xml);
    }
// end func getItem

} // end class TextFileToRssItem

/** TextFilesToRss
* Convert a text file (rss.txt) and text files provided with
* addFile(), and create a complete RSS 1.0 XML file.
* See Main() below for example.
*/
class TextFilesToRss extends TextFileToRssItem
{
    
// See TextFileToRssItem#rss_line_item
    
var $rss_meta_lines = array('update_period' => "daily",
                                
'update_frequency' => "1");
    var
$update_base = "1901-01-01T00:00+00:00";
    var
$output_file = "rss.xml";
    var
$meta_file = "rss.txt";
    var
$template_file = "template.xml";
    var
$files = array();
    var
$template = array(); // Lines of template file.

    
function TextFilesToRss($file)
    {
        
$author_at = array_search('author', array_keys($this->rss_lines));
        
$this->rss_lines = array_splice($this->rss_lines, 0, $author_at);
        
$this->rss_meta_lines = array_merge($this->rss_lines,
                                            
$this->rss_meta_lines);
        
$this->setOutputFile($file);
    }
// end func TextFileToRss

    
function setOutputFile($file)
    {
        
$this->output_file = $file;
    }
// end func setOutputFile

    
function setTemplate($file)
    {
        
$this->template_file = $file;
    }
// end func setTemplate

    
function setMetaFile($file)
    {
        
$this->meta_file = $file;
    }
// end func setMetaFile

    
function addFile($file)
    {
        
$this->files[] = $file;
    }
// end func addFile

    
function getFiles()
    {
        return
$this->files;
    }
// end func getFiles

    
function getXml()
    {
        
$meta = $this->getMetaData();
        if (
Main::instance_of($meta, 'Error')) {
            return
$meta;
        }
        
$rss = $meta;

        
$items = array();
        foreach (
$this->getFiles() as $f) {
            
$item = $this->processRssItemFile($f);
            if (
Main::instance_of($item, 'Error')) {
                return
$item;
            }
            list(
$url, $xml) = each($item);
            if (isset(
$items[$url])) {
                return new
Error("Duplicate RSS item",
                                 
"$url in $f");
            }
            
$items[$url] = $xml;
        }
        
$rss['item'] = $items;

        
$list_items = array_keys($items);
        
$rss['li'] = array();
        foreach (
$list_items as $li) {
            
$rss['li'][] = '<rdf:li rdf:resource="'
                         
. htmlspecialchars($li) . '"/>';
        }

        
$rss['file'] = $this->output_file;
            
$rss['update_base'] = $this->update_base;

        return
$this->mergeWithTemplateFile($rss);
    }
// end func getXml

    
function processRssItemFile($file)
    {
        
$txt2item = new TextFileToRssItem;
        
$item = $txt2item->getItem($file);
        return
$item;
    }
// end func processRssItemFile

    
function getMetaData()
    {
        
$this->rss_lines = $this->rss_meta_lines;
        
$meta = $this->getArray($this->meta_file);
        if (
Main::instance_of($meta, 'Error')) {
            return
$meta;
        }
        return
$meta;
    }
// end func getMetaData

    
function mergeWithTemplateFile($rss)
    {
        if (!
file_exists($this->template_file)) {
            return new
Error("Template file not found",
                             
$this->template_file);
        }
        if (!
is_readable($this->template_file)) {
            return new
Error("Template file not readable",
                             
$this->template_file);
        }
        
$this->template = file($this->template_file);
        
$prefixes = array();
        
$prefixes = $this->templateFileTagPrefixes();
        if (isset(
$rss['li'])) {
            
$prefix = isset($prefixes['li']) ? $prefixes['li'] : "";
            
$rss['li'] = join("\n$prefix", $rss['li']) . "\n";
        }
        if (isset(
$rss['item'])) {
            
$prefix = isset($prefixes['item']) ? $prefixes['item'] : "";
            
$rss['item'] = join("\n", $rss['item']);
            
$rss['item'] = str_replace("\n", "\n$prefix", $rss['item'])
                         .
"\n";
        }
        
$tags_2_re = create_function('$t', 'return "/%%".$t."%%\n?/";');
        
$tag_res = array_map($tags_2_re, array_keys($rss));
        
$xml = "";
        foreach (
$this->template as $line) {
            
$xml .= preg_replace($tag_res, array_values($rss), $line);
        }
        return
$xml . "\n";
    }
// end func mergeWithTemplateFile

    // Retrieve the prefixes for each tag.
    // This work is only done for getting the indentation right.
    // But it's worth it!
    
function templateFileTagPrefixes()
    {
        foreach (
$this->template as $line) {
            
preg_match_all("/^(.*)%%[^%]*?%%/", $line, &$p);
            while (isset(
$p[1]) && list($k, $prefix) = each($p[1])) {
                
preg_match_all("/%%([^%]*?)%%/", $line, &$m);
                while (isset(
$m[1]) && list($k, $tag) = each($m[1])) {
                    
$prefixes[$tag] = $prefix;
                }
            }
        }
        return
$prefixes;
    }
// end func templateFileTagPrefixes

} // end class TextFilesToRss

/** TextLineFile
* Each line is presumed the value of an array.
* Provide an array of keys to be given each value.
*/
class TextLineFile
{
    var
$lines;
    var
$lines_key;
    var
$n_fields;
    function
TextLineFile($key = null)
    {
        
$this->setLineKey($key);
    }
// end func TextLineFile

    
function setLineKey($key = null)
    {
        if (isset(
$key)) {
            
$this->lines_key = $key;
            
$n_fields = count($this->lines_key);
        }
    }
// end func setLineKey

    
function nFields()
    {
        return
$this->n_fields;
    }
// end func nFields

    
function parseFile($file)
    {
        
$this->TextLineFile();
        
$lines = file($file);
        if (
count($lines) <= $this->nFields()) {
            return new
Error("Incomplete text file", $file);
        }
        
$lines = array_map('chop', $lines);
        return
Main::array_combine(array_keys($this->lines_key),
                                   
$lines);
    }
// end func parseFile

} // end class TextLineFile

/** Error
* A simple error class.
*/  
class Error {
    var
$message;
    var
$problem;

    function
Error($msg, $prob)
    {
        
$this->message = $msg;
        
$this->problem = $prob;
    }
// end func Error

    
function text()
    {
        return
$this->message . ": " . $this->problem . "\n";
    }
// end func text

} // end class Error

/** Main
* The main routine, and some functions needed for PHP 4.0 compatability.
*/
class Main
{
    function
main($glob, $out)
    {
        
$rss = new TextFilesToRss($out);
        
$rss->setMetaFile("rss.txt");
        
$rss->setTemplate("rss.xml");
        
$files = array_reverse(glob($glob));
        foreach (
$files as $file) {
            
$rss->addFile($file);
        }
        
$str = $rss->getXml();
        if (
Main::instance_of($str, 'Error')) {
            if (
strpos(($http = $_SERVER['SERVER_PROTOCOL']), 'HTTP') !== false) {
                
header("$http 500 Internal Server Error");
            }
            
header("Content-type: text/plain");
            print
$str->text();
        } else {
            
header("Content-type: text/xml; charset=utf-8");
            print
$str;
        }
    }
// end func main

    
function instance_of($a, $b)
    {
        return (
is_object($a) && is_string($b)
                &&
get_class($a) === strtolower($b))
               || (
is_object($b) && is_string($a)
                   &&
get_class($b) === strtolower($a));
    }
// end func instance_of

    
function array_combine($keys, $values)
    {
        
$n = array(); // New array.
        
foreach ($keys as $k) {
            list(
$vkey, $v) = each($values);
            
$n[$k] = $v;
        }
        return
$n;
    }
// end func array_combine

} // end class Main

$main = new Main("event*.txt", "events.xml");

?>