steffen: server/kolab-horde-framework/kolab-horde-framework/Data/Data csv.php, NONE, 1.1 icalendar.php, NONE, 1.1 imc.php, NONE, 1.1 palm.php, NONE, 1.1 pdb.php, NONE, 1.1 tsv.php, NONE, 1.1 vcard.php, NONE, 1.1 vnote.php, NONE, 1.1 vtodo.php, NONE, 1.1

cvs at intevation.de cvs at intevation.de
Fri Oct 14 16:33:06 CEST 2005


Author: steffen

Update of /kolabrepository/server/kolab-horde-framework/kolab-horde-framework/Data/Data
In directory doto:/tmp/cvs-serv28903/kolab-horde-framework/kolab-horde-framework/Data/Data

Added Files:
	csv.php icalendar.php imc.php palm.php pdb.php tsv.php 
	vcard.php vnote.php vtodo.php 
Log Message:
Separated Horde Framework from kolab-resource-handlers

--- NEW FILE: csv.php ---
<?php
/**
 * Horde_Data implementation for comma-separated data (CSV).
 *
 * $Horde: framework/Data/Data/csv.php,v 1.28 2004/04/16 17:20:03 jan Exp $
 *
 * Copyright 1999-2004 Jan Schneider <jan at horde.org>
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
 *
 * @author  Jan Schneider <jan at horde.org>
 * @author  Chuck Hagenbuch <chuck at horde.org>
 * @version $Revision: 1.1 $
 * @since   Horde 1.3
 * @package Horde_Data
 */
class Horde_Data_csv extends Horde_Data {

    var $_extension = 'csv';
    var $_contentType = 'text/comma-separated-values';

    /**
     * Tries to dicover the CSV file's parameters.
     *
     * @access public
     * @param string $filename  The name of the file to investigate
     * @return array            An associative array with the following
     *                          possible keys:
     * 'sep':    The field seperator
     * 'quote':  The quoting character
     * 'fields': The number of fields (columns)
     */
    function discoverFormat($filename)
    {
        @include_once('File/CSV.php');
        if (class_exists('File_CSV')) {
            return File_CSV::discoverFormat($filename);
        } else {
            return array('sep' => ',');
        }
    }

    /**
     * Imports and parses a CSV file.
     *
     * @access public
     *
     * @param string  $filename  The name of the file to parse
     * @param boolean $header    Does the first line contain the field/column names?
     * @param string  $sep       The field/column seperator
     * @param string  $quote     The quoting character
     * @param integer $fields    The number or fields/columns
     *
     * @return array             A two-dimensional array of all imported data rows.
     *                           If $header was true the rows are associative arrays
     *                           with the field/column names as the keys.
     */
    function importFile($filename, $header = false, $sep = '', $quote = '', $fields = null)
    {
        @include_once('File/CSV.php');

        $data = array();

        /* File_CSV is present. */
        if (class_exists('File_CSV')) {
            /* File_CSV is a bit picky at what parameter it
               expects. */
            $conf = array();
            if (!empty($quote)) {
                $conf['quote'] = $quote;
            }
            if (empty($sep)) {
                $conf['sep'] = ',';
            } else {
                $conf['sep'] = $sep;
            }
            if ($fields) {
                $conf['fields'] = $fields;
            } else {
                return $data;
            }

            /* Strip and keep the first line if it contains the field
               names. */
            if ($header) {
                $head = File_CSV::read($filename, $conf);
            }

            while ($line = File_CSV::read($filename, $conf)) {
                if (!isset($head)) {
                    $data[] = $line;
                } else {
                    $newline = array();
                    for ($i = 0; $i < count($head); $i++) {
                        $newline[$head[$i]] = empty($line[$i]) ? '' : $line[$i];
                    }
                    $data[] = $newline;
                }
            }

            $fp = File_CSV::getPointer($filename, $conf);
            if ($fp) {
                rewind($fp);
            }

        /* Fall back to fgetcsv(). */
        } else {
            $fp = fopen($filename, 'r');
            if (!$fp) {
                return false;
            }

            /* Strip and keep the first line if it contains the field
               names. */
            if ($header) {
                $head = fgetcsv($fp, 1024, $sep);
            }
            while ($line = fgetcsv($fp, 1024, $sep)) {
                if (!isset($head)) {
                    $data[] = $line;
                } else {
                    $newline = array();
                    for ($i = 0; $i < count($head); $i++) {
                        $newline[$head[$i]] = empty($line[$i]) ? '' : $line[$i];
                    }
                    $data[] = $newline;
                }
            }

            fclose($fp);
        }
        return $data;
    }

    /**
     * Builds a CSV file from a given data structure and returns it as
     * a string.
     *
     * @access public
     *
     * @param array   $data       A two-dimensional array containing the data
     *                            set.
     * @param boolean $header     If true, the rows of $data are associative
     *                            arrays with field names as their keys.
     *
     * @return string  The CSV data.
     */
    function exportData($data, $header = false)
    {
        if (!is_array($data) || count($data) == 0) {
            return '';
        }

        $export = '';
        if ($header) {
            $head = current($data);
            foreach (array_keys($head) as $key) {
                if (!empty($key)) {
                    $export .= '"' . $key . '"';
                }
                $export .= ',';
            }
            $export = substr($export, 0, -1) . "\n";
        }

        foreach ($data as $row) {
            foreach ($row as $cell) {
                if (!empty($cell) || $cell === 0) {
                    $export .= '"' . $cell . '"';
                }
                $export .= ',';
            }
            $export = substr($export, 0, -1) . "\n";
        }

        return $export;
    }

    /**
     * Builds a CSV file from a given data structure and triggers its
     * download. It DOES NOT exit the current script but only outputs
     * the correct headers and data.
     *
     * @access public
     *
     * @param string  $filename   The name of the file to be downloaded.
     * @param array   $data       A two-dimensional array containing the data set.
     * @param boolean $header     If true, the rows of $data are associative arrays
     *                            with field names as their keys.
     */
    function exportFile($filename, $data, $header = false)
    {
        $export = $this->exportData($data, $header);
        $GLOBALS['browser']->downloadHeaders($filename, 'application/csv', false, strlen($export));
        echo $export;
    }

    /**
     * Takes all necessary actions for the given import step,
     * parameters and form values and returns the next necessary step.
     *
     * @access public
     *
     * @param integer $action        The current step. One of the IMPORT_*
     *                               constants.
     * @param optonal array $param   An associative array containing needed
     *                               parameters for the current step.
     *
     * @return mixed  Either the next step as an integer constant or imported
     *                data set after the final step.
     */
    function nextStep($action, $param = array())
    {
        switch ($action) {
        case IMPORT_FILE:
            $next_step = parent::nextStep($action, $param);
            if (is_a($next_step, 'PEAR_Error')) {
                return $next_step;
            }

            /* Move uploaded file so that we can read it again in the
               next step after the user gave some format details. */
            $file_name = Horde::getTempFile('import', false);
            if (!move_uploaded_file($_FILES['import_file']['tmp_name'], $file_name)) {
                return PEAR::raiseError(_("The uploaded file could not be saved."));
            }
            $_SESSION['import_data']['file_name'] = $file_name;

            /* Try to discover the file format ourselves. */
            $conf = $this->discoverFormat($file_name);
            if (!$conf) {
                $conf = array('sep' => ',');
            }
            $_SESSION['import_data'] = array_merge($_SESSION['import_data'], $conf);

            /* Read the file's first two lines to show them to the
               user. */
            $_SESSION['import_data']['first_lines'] = '';
            $fp = @fopen($file_name, 'r');
            if ($fp) {
                $line_no = 1;
                while ($line_no < 3 && $line = fgets($fp)) {
                    $newline = String::length($line) > 100 ? "\n" : '';
                    $_SESSION['import_data']['first_lines'] .= substr($line, 0, 100) . $newline;
                    $line_no++;
                }
            }
            return IMPORT_CSV;

        case IMPORT_CSV:
            $_SESSION['import_data']['header'] = Util::getFormData('header');
            $import_data = $this->importFile($_SESSION['import_data']['file_name'],
                                             $_SESSION['import_data']['header'],
                                             Util::getFormData('sep'),
                                             Util::getFormData('quote'),
                                             Util::getFormData('fields'));
            $_SESSION['import_data']['data'] = $import_data;
            unset($_SESSION['import_data']['map']);
            return IMPORT_MAPPED;

        default:
            return parent::nextStep($action, $param);
        }
    }

}

--- NEW FILE: icalendar.php ---
<?php

/** We rely on the Horde_Data_imc:: abstract class. */
require_once dirname(__FILE__) . '/imc.php';

/**
 * This is iCalendar (vCalendar).
 *
 * $Horde: framework/Data/Data/icalendar.php,v 1.29 2004/03/19 14:50:46 chuck Exp $
 *
 * Copyright 1999-2004 Chuck Hagenbuch <chuck at horde.org>
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
 *
 * @author  Chuck Hagenbuch <chuck at horde.org>
 * @package Horde_Data
 * @since   Horde 3.0
 */
class Horde_Data_icalendar extends Horde_Data_imc {

    var $_params = array();

    function _build($data, &$i, $return = false)
    {
        // We shouldn't call this unless we're about to begin an
        // object of some sort.
        $uname = String::upper($data[$i]['name']);
        if ($uname != 'BEGIN') {
            return PEAR::raiseError(sprintf(_("Import Error: Expecting BEGIN on line %d."), $i));
        }

        $object = array('type' => String::upper($data[$i]['values'][0]),
                        'objects' => array(),
                        'params' => array());
        $i++;

        while (String::upper($data[$i]['name']) != 'END') {
            if (String::upper($data[$i]['name']) == 'BEGIN') {
                $object['objects'][] = $this->_build($data, $i, true);
            } else {
                $object['params'][String::upper($data[$i]['name'])] = array('params' => $data[$i]['params'],
                                                                            'values' => $data[$i]['values']);
            }
            $i++;
        }

        if (String::upper($data[$i]['values'][0]) != $object['type']) {
            return PEAR::raiseError(sprintf(_("Import Error: Mismatch; expecting END:%s on line %d"), $object['type'], $i));
        }

        if ($return) {
            return $object;
        } else {
            if (String::upper($object['type']) == 'VCALENDAR') {
                $this->_objects = $object['objects'];
                $this->_params = $object['params'];
            } else {
                $this->_objects[] = $object;
            }
        }
    }

    function getValues($attribute, $event = 0)
    {
        $values = array();
        $attribute = String::upper($attribute);

        if (isset($this->_objects[$event]['params'][$attribute])) {
            $count = count($this->_objects[$event]['params'][$attribute]['values']);
            for ($i = 0; $i < $count; $i++) {
                $values[$i] = $this->read($this->_objects[$event]['params'][$attribute], $i);
            }
        }
        return (count($values) > 0) ? $values : null;
    }

    function getAttributes($event = 0)
    {
        return array_keys($this->_objects[$event]['params']);
    }

    /**
     * Builds an iCalendar file from a given data structure and returns it as
     * a string.
     *
     * @access public
     *
     * @param array $data     A two-dimensional array containing the data set.
     * @param string $method  (optional) The iTip method to use.
     *
     * @return string      The iCalendar data.
     */
    function exportData($data, $method = 'REQUEST')
    {
        global $prefs;

        $DST = date('I');
        $TZNAME = date('T');
        $TZID = $prefs->getValue('timezone');
        $TZOFFSET = date('O');

        // These can be used later in a VTIMEZONE object:
        // $TZOffsetFrom = ($DST) ? $TZOFFSET - 100 : $TZOFFSET;
        // $TZOffsetTo   = ($DST) ? $TZOFFSET : $TZOFFSET - 100;

        $file = implode($this->_newline, array('BEGIN:VCALENDAR',
                                               'VERSION:2.0',
                                               'PRODID:-//Horde.org//Kronolith Generated',
                                               'METHOD:' . $method)) . $this->_newline;
        foreach ($data as $row) {
            $file .= 'BEGIN:VEVENT' . $this->_newline;
            foreach ($row as $key => $val) {
                if (!empty($val)) {
                    // Basic encoding. Newlines for now; more work
                    // here to make this RFC-compliant.
                    $file .= $key . ':' .  $this->_quoteAndFold($val);
                }
            }
            $file .= 'END:VEVENT' . $this->_newline;
        }
        $file .= 'END:VCALENDAR' . $this->_newline;

        return $file;
    }

    /**
     * Builds an iCalendar file from a given data structure and
     * triggers its download.  It DOES NOT exit the current script but
     * only outputs the correct headers and data.
     *
     * @access public
     * @param string $filename   The name of the file to be downloaded.
     * @param array $data        A two-dimensional array containing the data set.
     */
    function exportFile($filename, $data)
    {
        $export = $this->exportData($data);
        $GLOBALS['browser']->downloadHeaders($filename, 'text/calendar', false, strlen($export));
        echo $export;
    }

    function toHash($i = 0)
    {
        $hash = array();
        if (($title = $this->getValues('SUMMARY', $i)) !== null) {
            $hash['title'] = implode("\n", $title);
        }
        if (($desc = $this->getValues('DESCRIPTION', $i)) !== null) {
            $hash['description'] = implode("\n", $desc);
        }
        if (($location = $this->getValues('LOCATION', $i)) !== null) {
            $hash['location'] = implode("\n", $location);
        }

        if (($start = $this->getValues('DTSTART', $i)) !== null &&
            count($start) == 1) {
            $start = $this->mapDate($start[0]);
            $hash['start_date'] = $start['year'] . '-' . $start['month'] . '-' . $start['mday'];
            $hash['start_time'] = $start['hour'] . ':' . $start['min'] . ':' . $start['sec'];
        }

        if (($start = $this->getValues('DURATION', $i)) != null &&
            count($start) == 1) {
            preg_match('/^P([0-9]{1,2}[W])?([0-9]{1,2}[D])?([T]{0,1})?([0-9]{1,2}[H])?([0-9]{1,2}[M])?([0-9]{1,2}[S])?/', $start[0], $duration);
            $hash['duration'] = $duration;
        }

        if (($start = $this->getValues('DTEND', $i)) !== null &&
            count($start) == 1) {
            $end = $this->mapDate($start[0]);
            $hash['end_date'] = $end['year'] . '-' . $end['month'] . '-' . $end['mday'];
            $hash['end_time'] = $end['hour'] . ':' . $end['min'] . ':' . $end['sec'];
        }

        return $hash;
    }

    /**
     * Takes all necessary actions for the given import step,
     * parameters and form values and returns the next necessary step.
     *
     * @access public
     *
     * @param integer $action        The current step. One of the IMPORT_*
     *                               constants.
     * @param optional array $param  An associative array containing needed
     *                               parameters for the current step.
     * @return mixed  Either the next step as an integer constant or imported
     *                data set after the final step.
     */
    function nextStep($action, $param = array())
    {
        switch ($action) {
        case IMPORT_FILE:
            $next_step = parent::nextStep($action, $param);
            if (is_a($next_step, 'PEAR_Error')) {
                return $next_step;
            }

            $import_data = $this->importFile($_FILES['import_file']['tmp_name']);
            if (is_a($import_data, 'PEAR_Error')) {
                return $import_data;
            }
            /* Build the result data set as an associative array. */
            $data = array();
            for ($i = 0; $i < $this->count(); $i++) {
                $data[] = $this->toHash($i);
            }
            return $data;
            break;

        default:
            return parent::nextStep($action, $param);
            break;
        }
    }

}

--- NEW FILE: imc.php ---
<?php
/**
 * Abstract implementation of the Horde_Data:: API for IMC data -
 * vCards and iCalendar data, etc. Provides a number of utility
 * methods that vCard and iCalendar implementation can share and rely
 * on.
 *
 * $Horde: framework/Data/Data/imc.php,v 1.29 2004/03/16 21:39:41 chuck Exp $
 *
 * Copyright 1999-2004 Jan Schneider <jan at horde.org>
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
 *
 * @author  Jan Schneider <jan at horde.org>
 * @package Horde_Data
 * @since   Horde 3.0
 */
class Horde_Data_imc extends Horde_Data {

    var $_objects = array();

    /**
     * According to RFC 2425, we should always use CRLF-terminated
     * lines.
     * @var string $_newline
     */
    var $_newline = "\r\n";

    function importData($text)
    {
        $lines = preg_split('/(\r\n|\n|\r)/', $text);
        $data = array();

        // Unfolding.
        $countData = 0;
        foreach ($lines as $line) {
            if (preg_match('/^[ \t]/', $line) && $countData > 1) {
                $data[$countData - 1] .= substr($line, 1);
            } elseif (trim($line) != '') {
                $data[] = $line;
                $countData++;
            }
            $data[$countData - 1] = trim($data[$countData - 1]);
        }

        $lines = $data;
        $data = array();
        foreach ($lines as $line) {
            $line = preg_replace('/"([^":]*):([^":]*)"/', "\"\\1\x00\\2\"", $line);
            if (!strstr($line, ':')) {
                return PEAR::raiseError(_("Import Error: Malformed line."));
            }
            list($name, $value) = explode(':', $line, 2);
            $name = preg_replace('/\0/', ':', $name);
            $value = preg_replace('/\0/', ':', $value);
            $name = explode(';', $name);
            $params = array();
            if (isset($name[1])) {
                $iMax = count($name);
                for ($i = 1; $i < $iMax; $i++) {
                    $name_value = explode('=', $name[$i]);
                    $paramname = $name_value[0];
                    $paramvalue = isset($name_value[1]) ? $name_value[1] : null;
                    if (isset($paramvalue)) {
                        preg_match_all('/("((\\\\"|[^"])*)"|[^,]*)(,|$)/', $paramvalue, $split);
                        for ($j = 0; $j < count($split[1]) - 1; $j++) {
                            $params[$paramname][] = stripslashes($split[1][$j]);
                        }
                    } else {
                        $params[$paramname] = true;
                    }
                }
            }

            // Store unsplitted value for vCard 2.1.
            $value21 = $value;

            $value = preg_replace('/\\\\,/', "\x00", $value);
            $values = explode(',', $value);
            for ($i = 0; $i < count($values); $i++) {
                $values[$i] = preg_replace('/\0/', ',', $values[$i]);
                $values[$i] = preg_replace('/\\\\n/', "\n", $values[$i]);
                $values[$i] = preg_replace('/\\\\,/', ',', $values[$i]);
                $values[$i] = preg_replace('/\\\\\\\\/', '\\', $values[$i]);
            }

            $data[] = array('name' => $name[0],
                            'params' => $params,
                            'values' => $values,
                            'value21' => $value21);
        }
        $start = 0;
        $this->_build($data, $start);
        return $this->_objects;
    }

    function read($attribute, $index = 0)
    {
        $value = $attribute['values'][$index];

        if (isset($attribute['params']['ENCODING'])) {
            switch ($attribute['params']['ENCODING'][0]) {
            case 'QUOTED-PRINTABLE':
                $value = quoted_printable_decode($value);
                break;
            }
        }

        if (isset($attribute['params']['QUOTED-PRINTABLE']) && ($attribute['params']['QUOTED-PRINTABLE'] == true)) {
            $value = quoted_printable_decode($value);
        }

        if (isset($attribute['params']['CHARSET'])) {
            $value = String::convertCharset($value, $attribute['params']['CHARSET'][0]);
        } else {
            // As per RFC 2279, assume UTF8 if we don't have an
            // explicit charset parameter.
            $value = String::convertCharset($value, 'utf-8');
        }

        return $value;
    }

    function readAll($attribute)
    {
        $count = count($attribute['values']);
        $value = '';
        for ($i = 0; $i < $count; $i++) {
            $value .= $this->read($attribute, $i) . $this->_newline;
        }

        return substr($value, 0, -(strlen($this->_newline)));
    }

    function makeDate($dateOb)
    {
        // FIXME: We currently handle only "full" offsets, not TZs
        // like +1030.
        $TZOffset = substr(date('O'), 0, 3);
        $thisHour = $dateOb->hour - $TZOffset;
        if ($thisHour < 0) {
            require_once 'Date/Calc.php';
            $prevday = Date_Calc::prevDay($dateOb->mday, $dateOb->month, $dateOb->year);
            $dateOb->mday = substr($prevday, 6, 2);
            $dateOb->month = substr($prevday, 4, 2);
            $dateOb->year = substr($prevday, 0, 4);
            $thisHour += 24;
        }
        return sprintf('%04d%02d%02dT%02d%02d%02dZ',
                       $dateOb->year,
                       $dateOb->month,
                       $dateOb->mday,
                       $thisHour,
                       $dateOb->min,
                       $dateOb->sec);
    }

    function makeDuration($seconds)
    {
        $duration = '';
        if ($seconds < 0) {
            $duration .= '-';
            $seconds *= -1;
        }
        $duration .= 'P';
        $days = floor($seconds / 86400);
        $seconds = $seconds % 86400;
        $weeks = floor($days / 7);
        $days = $days % 7;
        if ($weeks) {
            $duration .= $weeks . "W";
        }
        if ($days) {
            $duration .= $days . "D";
        }
        if ($seconds) {
            $duration .= 'T';
            $hours = floor($seconds / 3600);
            $seconds = $seconds % 3600;
            if ($hours > 0) {
                $duration .= $hours . 'H';
            }
            $minutes = floor($seconds / 60);
            $seconds = $seconds % 60;
            if ($minutes) {
                $duration .= $minutes . 'M';
            }
            if ($seconds) {
                $seconds .= $seconds . 'S';
            }
        }
        return $duration;
    }

    function mapDate($datestring)
    {
        if (strpos($datestring, 'T') !== false) {
            list($date, $time) = explode('T', $datestring);
        } else {
            $date = $datestring;
        }

        if (strlen($date) == 10) {
            $dates = explode('-', $date);
        } else {
            $dates = array();
            $dates[] = substr($date, 0, 4);
            $dates[] = substr($date, 4, 2);
            $dates[] = substr($date, 6, 2);
        }

        $dateOb = array('mday' => $dates[2],
                        'month' => $dates[1],
                        'year' => $dates[0]);

        if (isset($time)) {
            @list($time, $zone) = explode('Z', $time);
            if (strstr($time, ':') !== false) {
                $times = explode(':', $time);
            } else {
                $times = array(substr($time, 0, 2),
                               substr($time, 2, 2),
                               substr($time, 4));
            }
            if (isset($zone)) {
                // Map the timezone here.
            }

            $TZOffset = substr(date('O'), 0, 3);
            $dateOb['hour'] = $times[0] + $TZOffset;
            $dateOb['min'] = $times[1];
            $dateOb['sec'] = $times[2];
        } else {
            $dateOb['hour'] = 0;
            $dateOb['min'] = 0;
            $dateOb['sec'] = 0;
        }

        // Put a timestamp in here, too.
        $func = isset($zone) ? 'gmmktime' : 'mktime';
        $dateOb['ts'] = $func($dateOb['hour'], $dateOb['min'], $dateOb['sec'],
                              $dateOb['month'], $dateOb['mday'], $dateOb['year']);

        return $dateOb;
    }

    function count()
    {
        return count($this->_objects);
    }

    function toHash()
    {
        return array();
    }

    function fromHash()
    {
        return array();
    }

    function _quoteAndFold($string)
    {
        $lines = preg_split('/(\r\n|\n|\r)/', rtrim($string));
        $valueLines = array();
        foreach ($lines as $line) {
            if (strlen($line) > 75) {
                $foldedline = '';
                $firstline = true;
                while (!empty($line)) {
                    /* Make first line shorter to allow for the field
                     * name. */
                    if ($firstline) {
                        $len = 60;
                        $firstline = false;
                    } else {
                        $len = 75;
                    }

                    $foldedline .= (empty($foldedline)) ? substr($line, 0, $len) : $this->_newline . ' ' . substr($line, 0, $len);
                    if (strlen($line) <= $len) {
                        $line = '';
                    } else {
                        $line = substr($line, $len);
                    }
                }
                $valueLines[] = $foldedline;
            } else {
                $valueLines[] = $line;
            }
        }

        return implode($this->_newline . ' ', $valueLines) . $this->_newline;
    }

}

--- NEW FILE: palm.php ---
<?php
/**
 * Abstract class to allow data exchange between the Horde
 * applications and various Palm formats.
 *
 * $Horde: framework/Data/Data/palm.php,v 1.4 2003/11/06 15:26:25 chuck Exp $
 *
 * TODO: export method
 *
 * @author  Mathieu Clabaut <mathieu.clabaut at free.fr>
 * @package Horde_Data
 */
class Data_palm extends Data {

}

/**
 * PHP-PDB -- PHP class to write PalmOS databases.
 *
 * Copyright (C) 2001 - PHP-PDB development team
 * Licensed under the GNU LGPL software license.
 * See the doc/LEGAL file for more information
 * See http://php-pdb.sourceforge.net/ for more information about the library
 *
 * As a note, storing all of the information as hexadecimal kinda
 * sucks, but it is tough to store and properly manipulate a binary
 * string in PHP. We double the size of the data but decrease the
 * difficulty level immensely.
 */

/**
 * Define constants
 */

// Sizes
define('PDB_HEADER_SIZE', 72); // Size of the database header
define('PDB_INDEX_HEADER_SIZE', 6); // Size of the record index header
define('PDB_RECORD_HEADER_SIZE', 8); // Size of the record index entry
define('PDB_RESOURCE_SIZE', 10);  // Size of the resource index entry
define('PDB_EPOCH_1904', 2082844800); // Difference between Palm's time and Unix

// Attribute Flags
define('PDB_ATTRIB_RESOURCE', 1);
define('PDB_ATTRIB_READ_ONLY', 2);
define('PDB_ATTRIB_APPINFO_DIRTY', 4);
define('PDB_ATTRIB_BACKUP', 8);
define('PDB_ATTRIB_OK_NEWER', 16);
define('PDB_ATTRIB_RESET', 32);
define('PDB_ATTRIB_OPEN', 64);
define('PDB_ATTRIB_LAUNCHABLE', 512);

// Record Flags
// The first nibble is reserved for the category number
// See PDB_CATEGORY_MASK
define('PDB_RECORD_ATTRIB_PRIVATE', 16);
define('PDB_RECORD_ATTRIB_DELETED', 32);
define('PDB_RECORD_ATTRIB_DIRTY', 64);
define('PDB_RECORD_ATTRIB_EXPUNGED', 128);

// Category support
define('PDB_CATEGORY_NUM', 16);  // Number of categories
define('PDB_CATEGORY_NAME_LENGTH', 16);  // Bytes allocated for name
define('PDB_CATEGORY_SIZE', 276); // 2 + (num * length) + num + 1 + 1
define('PDB_CATEGORY_MASK', 15);  // Bitmask -- use with attribute of record
                                  // to get the category ID


// Double conversion
define('PDB_DOUBLEMETHOD_UNTESTED', 0);
define('PDB_DOUBLEMETHOD_NORMAL', 1);
define('PDB_DOUBLEMETHOD_REVERSE', 2);
define('PDB_DOUBLEMETHOD_BROKEN', 3);

/**
 * PalmDB Class
 *
 * Contains all of the required methods and variables to write a pdb file.
 * Extend this class to provide functionality for memos, addresses, etc.
 *
 * @package Horde_Data
 */
class PalmDB {
   var $Records = array();     // All of the data from the records is here
                               // Key = record ID
   var $RecordAttrs = array(); // And their attributes are here
   var $CurrentRecord = 1;     // Which record we are currently editing
   var $Name = '';             // Name of the PDB file
   var $TypeID = '';           // The 'Type' of the file (4 chars)
   var $CreatorID = '';        // The 'Creator' of the file (4 chars)
   var $Attributes = 0;        // Attributes (bitmask)
   var $Version = 0;           // Version of the file
   var $ModNumber = 0;         // Modification number
   var $CreationTime = 0;      // Stored in unix time (Jan 1, 1970)
   var $ModificationTime = 0;  // Stored in unix time (Jan 1, 1970)
   var $BackupTime = 0;        // Stored in unix time (Jan 1, 1970)
   var $AppInfo = '';          // Basic AppInfo block
   var $SortInfo = '';         // Basic SortInfo block
   var $DoubleMethod = PDB_DOUBLEMETHOD_UNTESTED;
                               // What method to use for converting doubles


   // Creates a new database class
   function PalmDB($Type = '', $Creator = '', $Name = '') {
      $this->TypeID = $Type;
      $this->CreatorID = $Creator;
      $this->Name = $Name;
      $this->CreationTime = time();
      $this->ModificationTime = time();
   }


   /*
    * Data manipulation functions
    *
    * These convert various numbers and strings into the hexadecimal
    * format that is used internally to construct the file.  We use hex
    * encoded strings since that is a lot easier to work with than binary
    * data in strings, and we can easily tell how big the true value is.
    * B64 encoding does some odd stuff, so we just make the memory
    * consumption grow tremendously and the complexity level drops
    * considerably.
    */
       
   // Converts a byte and returns the value
   function Int8($value) {
      $value &= 0xFF;
      return sprintf("%02x", $value);
   }
   
   
   // Loads a single byte as a number from the file
   // Use if you want to make your own ReadFile function
   function LoadInt8($file) {
      if (is_resource($file))
         $string = fread($file, 1);
      else
         $string = $file;
      return ord($string[0]);
   }
   
   
   // Converts an integer (two bytes) and returns the value
   function Int16($value) {
      $value &= 0xFFFF;
      return sprintf("%02x%02x", $value / 256, $value % 256);
   }
   
   
   // Loads two bytes as a number from the file
   // Use if you want to make your own ReadFile function
   function LoadInt16($file) {
      if (is_resource($file))
         $string = fread($file, 2);
      else
         $string = $file;
      return ord($string[0]) * 256 + ord($string[1]);
   }
   
   
   // Converts an integer (three bytes) and returns the value
   function Int24($value) {
      $value &= 0xFFFFFF;
      return sprintf("%02x%02x%02x", $value / 65536, 
                     ($value / 256) % 256, $value % 256);
   }


   // Loads three bytes as a number from the file
   // Use if you want to make your own ReadFile function
   function LoadInt24($file) {
      if (is_resource($file))
         $string = fread($file, 3);
      else
     $string = $file;
      return ord($string[0]) * 65536 + ord($string[1]) * 256 +
         ord($string[2]);
   }
      
      
   // Converts an integer (four bytes) and returns the value
   // 32-bit integers have problems with PHP when they are bigger than
   // 0x80000000 (about 2 billion) and that's why I don't use pack() here
   function Int32($value) {
      $negative = false;
      if ($value < 0) {
         $negative = true;
     $value = - $value;
      }
      $big = $value / 65536;
      settype($big, 'integer');
      $little = $value - ($big * 65536);
      if ($negative) {
         // Little must contain a value
         $little = - $little;
     // Big might be zero, and should be 0xFFFF if that is the case.
     $big = 0xFFFF - $big;
      }
      $value = PalmDB::Int16($big) . PalmDB::Int16($little);
      return $value;
   }
   
   
   // Loads a four-byte string from a file into a number
   // Use if you want to make your own ReadFile function
   function LoadInt32($file) {
      if (is_resource($file))
         $string = fread($file, 4);
      else
         $string = $file;
      $value = 0;
      $i = 0;
      while ($i < 4) {
         $value *= 256;
     $value += ord($string[$i]);
     $i ++;
      }
      return $value;
   }
   
   
   // Converts the number into a double and returns the encoded value
   // Not sure if this will work on all platforms.
   // Double(10.53) should return "40250f5c28f5c28f"
   function Double($value) {
      if ($this->DoubleMethod == PDB_DOUBLEMETHOD_UNTESTED) {
         $val = bin2hex(pack('d', 10.53));
     $val = strtolower($val);
     if (substr($val, 0, 4) == '8fc2')
        $this->DoubleMethod = PDB_DOUBLEMETHOD_REVERSE;
     if (substr($val, 0, 4) == '4025')
        $this->DoubleMethod = PDB_DOUBLEMETHOD_NORMAL;
     if ($this->DoubleMethod == PDB_DOUBLEMETHOD_UNTESTED)
        $this->DoubleMethod = PDB_DOUBLEMETHOD_BROKEN;
      }
      
      if ($this->DoubleMethod == PDB_DOUBLEMETHOD_BROKEN)
         return '0000000000000000';
     
      $value = bin2hex(pack('d', $value));
      
      if ($this->DoubleMethod == PDB_DOUBLEMETHOD_REVERSE)
         $value = substr($value, 14, 2) . substr($value, 12, 2) . 
            substr($value, 10, 2) . substr($value, 8, 2) . 
        substr($value, 6, 2) . substr($value, 4, 2) . 
        substr($value, 2, 2) . substr($value, 0, 2);
        
      return $value;
   }
   
   
   // The reverse?  Not coded yet.
   // Use if you want to make your own ReadFile function
   function LoadDouble($file) {
      if (is_resource($file))
         $string = fread($file, 8);
      else
         $string = $file;
      return 0;
   }
   
   
   // Converts a string into hexadecimal.
   // If $maxLen is specified and is greater than zero, the string is 
   // trimmed and will contain up to $maxLen characters.
   // String("abcd", 2) will return "ab" hex encoded (a total of 4
   // resulting bytes, but 2 encoded characters).
   // Returned string is *not* NULL-terminated.
   function String($value, $maxLen = false) {
      $value = bin2hex($value);
      if ($maxLen !== false && $maxLen > 0)
         $value = substr($value, 0, $maxLen * 2);
      return $value;
   }
   
   
   // Pads a hex-encoded value (typically a string) to a fixed size.
   // May grow too long if $value starts too long
   // $value = hex encoded value
   // $minLen = Append nulls to $value until it reaches $minLen
   // $minLen is the desired size of the string, unencoded.
   // PadString('6162', 3) results in '616200' (remember the hex encoding)
   function PadString($value, $minLen) {
      $PadBytes = '00000000000000000000000000000000';
      $PadMe = $minLen - (strlen($value) / 2);
      while ($PadMe > 0) {
         if ($PadMe > 16)
        $value .= $PadBytes;
     else
        return $value . substr($PadBytes, 0, $PadMe * 2);
           
     $PadMe = $minLen - (strlen($value) / 2);
      }
      
      return $value;
   }
   
   
   /*
    * Record manipulation functions
    */
    
   // Sets the current record pointer to the new record number if an
   // argument is passed in.
   // Returns the old record number (just in case you want to jump back)
   // Does not do basic record initialization if we are going to a new 
   // record.
   function GoToRecord($num = false) {
      if ($num === false)
         return $this->CurrentRecord;
      if (gettype($num) == 'string' && ($num[0] == '+' || $num[0] == '-'))
         $num = $this->CurrentRecord + $num;
      $oldRecord = $this->CurrentRecord;
      $this->CurrentRecord = $num;
      return $oldRecord;
   }
   
   
   // Returns the size of the current record if no arguments.
   // Returns the size of the specified record if arguments.
   function GetRecordSize($num = false) {
      if ($num === false)
         $num = $this->CurrentRecord;
      if (! isset($this->Records[$num]))
         return 0;
      return strlen($this->Records[$num]) / 2;
   }
   
   
   // Adds to the current record.  The input data must be already
   // hex encoded.  Initializes the record if it doesn't exist.
   function AppendCurrent($value) {
      if (! isset($this->Records[$this->CurrentRecord]))
         $this->Records[$this->CurrentRecord] = '';
      $this->Records[$this->CurrentRecord] .= $value;
   }
   
   
   // Adds a byte to the current record
   function AppendInt8($value) {
      $this->AppendCurrent($this->Int8($value));
   }
   
   
   // Adds an integer (2 bytes) to the current record
   function AppendInt16($value) {
      $this->AppendCurrent($this->Int16($value));
   }
   
   
   // Adds an integer (4 bytes) to the current record
   function AppendInt32($value) {
      $this->AppendCurrent($this->Int32($value));
   }
   
   
   // Adds a double to the current record
   function AppendDouble($value) {
      $this->AppendCurrent($this->Double($value));
   }
   
   
   // Adds a string (not NULL-terminated)
   function AppendString($value, $maxLen = false) {
      $this->AppendCurrent($this->String($value, $maxLen));
   }
   
   
   // Returns true if the specified/current record exists and is set
   function RecordExists($Rec = false) {
      if ($Rec === false)
         $Rec = $this->CurrentRecord;
      if (isset($this->Records[$Rec]))
         return true;
      return false;
   }
   
   
   // Returns the hex-encoded data for the specified record or the current
   // record if not specified
   function GetRecord($Rec = false) {
      if ($Rec === false)
         $Rec = $this->CurrentRecord;
      if (isset($this->Records[$Rec]))
         return $this->Records[$Rec];
      return '';
   }
   
   
   // Returns the raw data inside the current/specified record.  Use this
   // for odd record types (like a Datebook record).  Also, use this
   // instead of just using $PDB->Records[] directly.
   function GetRecordRaw($Rec = false) {
      if ($Rec === false)
         $Rec = $this->CurrentRecord;
      if (isset($this->Records[$Rec]))
         return $this->Records[$Rec];
      return false;
   }
   
   
   // Sets the hex-encoded data (or whatever) for the current record
   // Use this instead of the Append* functions if you have an odd
   // type of record (like a Datebook record).
   // Also, use this instead of just setting $PDB->Records[]
   // directly.
   // SetRecordRaw('data');
   // SetRecordRaw(24, 'data');   (specifying the record num)
   function SetRecordRaw($A, $B = false) {
      if ($B === false) {
         $B = $A;
     $A = $this->CurrentRecord;
      }
      $this->Records[$A] = $B;
   }
   
   
   // Deletes the current record
   // You are urged to use GoToRecord() and jump to an existing record
   // after this function call so that the deleted record doesn't
   // get accidentally recreated/used -- all append functions will
   // create a new, empty record if the current record doesn't exist.
   function DeleteCurrentRecord() {
      if (isset($this->Records[$this->CurrentRecord]))
         unset($this->Records[$this->CurrentRecord]);
      if (isset($this->RecordAttrs[$this->CurrentRecord]))
         unset($this->RecordAttrs[$this->CurrentRecord]);
   }
   
   
   // Returns an array of available record IDs in the order they should
   // be written.
   // Probably should only be called within the class.
   function GetRecordIDs() {
      $keys = array_keys($this->Records);
      if (! is_array($keys) || count($keys) < 1)
         return array();
      sort($keys, SORT_NUMERIC);
      return $keys;
   }
   
   
   // Returns the number of records.  This should match the number of
   // keys returned by GetRecordIDs().
   function GetRecordCount() {
      return count($this->Records);
   }
   
   
   // Returns the size of the AppInfo block.
   // Used only for writing
   function GetAppInfoSize() {
      if (! isset($this->AppInfo))
         return 0;
      return strlen($this->AppInfo) / 2;
   }
   
   
   // Returns the AppInfo block (hex encoded)
   // Used only for writing
   function GetAppInfo() {
      if (! isset($this->AppInfo))
         return 0;
      return $this->AppInfo;
   }
   
   
   // Returns the size of the SortInfo block
   // Used only for writing
   function GetSortInfoSize() {
      if (! isset($this->SortInfo))
         return 0;
      return strlen($this->SortInfo) / 2;
   }
   
   
   // Returns the SortInfo block (hex encoded)
   // Used only for writing
   function GetSortInfo() {
      if (! isset($this->SortInfo))
         return 0;
      return $this->SortInfo;
   }
   
   
   /*
    * Category Support
    */
    
   // Creates the hex-encoded data to be stuck in the AppInfo
   // block if the database supports categories.
   //
   // Data format:
   //    $categoryArray[id#] = name
   // Or:
   //    $categoryArray[id#]['name'] = name
   //    $categoryArray[id#]['renamed'] = true / false
   //
   // Tips:
   //  * I'd suggest numbering your categories sequentially
   //  * Do not have a category 0.  It must always be 'Unfiled'.  This
   //    function will overwrite any category with the ID of 0.
   //  * There is a maximum of 16 categories, including 'Unfiled'.
   //
   // Category 0 is reserved for 'Unfiled'
   // Categories 1-127 are used for handheld ID numbers
   // Categories 128-255 are used for desktop ID numbers
   // Do not let category numbers be created larger than 255
   function CreateCategoryData($CategoryArray) {
      $CategoryArray[0] = array('Name' => 'Unfiled', 'Renamed' => false);
      $CatsWritten = 0;
      $LastIdWritten = 0;
      $RenamedFlags = 0;
      $CategoryStr = '';
      $IdStr = '';
      $keys = array_keys($CategoryArray);
      sort($keys);
      foreach ($keys as $id) {
         if ($CatsWritten < PDB_CATEGORY_NUM) {
        $CatsWritten ++;
        $LastIdWritten = $id;
        $RenamedFlags *= 2;
        if (is_array($CategoryArray[$id]) && 
            isset($CategoryArray[$id]['Renamed']) &&
        $CategoryArray[$id]['Renamed'])
           $RenamedFlags += 1;
        $name = '';
        if (is_array($CategoryArray[$id])) {
           if (isset($CategoryArray[$id]['Name']))
              $name = $CategoryArray[$id]['Name'];
        } else
           $name = $CategoryArray[$id];
        $name = $this->String($name, PDB_CATEGORY_NAME_LENGTH);
        $CategoryStr .= $this->PadString($name,
                                         PDB_CATEGORY_NAME_LENGTH);
        $IdStr .= $this->Int8($id);
     }
      }
     
      while ($CatsWritten < PDB_CATEGORY_NUM) {
         $CatsWritten ++;
     $LastIdWritten ++;
     $RenamedFlags *= 2;
     $CategoryStr .= $this->PadString('', PDB_CATEGORY_NAME_LENGTH);
     $IdStr .= $this->Int8($LastIdWritten);
      }
      
      $TrailingBytes = $this->Int8($LastIdWritten);
      $TrailingBytes .= $this->Int8(0);
     
      // Error checking
      if ($LastIdWritten >= 256)
         return $this->PadString('', PDB_CATEGORY_SIZE);
     
      return $this->Int16($RenamedFlags) . $CategoryStr . $IdStr . 
         $TrailingBytes;
   }
   
   
   // This should be called by other subclasses that use category support
   // It returns a category array.  Each element in the array is another
   // array with the key 'name' set to the name of the category and 
   // the key 'renamed' set to the renamed flag for that category.
   function LoadCategoryData($fileData) {
      $RenamedFlags = $this->LoadInt16(substr($fileData, 0, 2));
      $Offset = 2;
      $StartingFlag = 65536;
      $Categories = array();
      while ($StartingFlag > 1) {
         $StartingFlag /= 2;
     $Name = substr($fileData, $Offset, PDB_CATEGORY_NAME_LENGTH);
     $i = 0;
     while ($i < PDB_CATEGORY_NAME_LENGTH && $Name[$i] != "\0")
        $i ++;
     if ($i == 0)
        $Name = '';
     elseif ($i < PDB_CATEGORY_NAME_LENGTH)
        $Name = substr($Name, 0, $i);
     if ($RenamedFlags & $StartingFlag)
        $RenamedFlag = true;
     else
        $RenamedFlag = false;
     $Categories[] = array('Name' => $Name, 'Renamed' => $RenamedFlag);
     $Offset += PDB_CATEGORY_NAME_LENGTH;
      }
      
      $CategoriesParsed = array();
      
      foreach ($Categories as $CategoryData) {
         $UID = $this->LoadInt8(substr($fileData, $Offset, 1));
     $Offset ++;
     if ($CategoryData['Name'] != '')
        $CategoriesParsed[$UID] = $CategoryData;
      }
      
      // Ignore the last ID
      return $CategoriesParsed;
   }
   
   
   /*
    * Database Writing Functions
    */
   
   // *NEW*
   // Takes a hex-encoded string and makes sure that when decoded, the data
   // lies on a four-byte boundary.  If it doesn't, it pads the string with
   // NULLs
   /*
    * Commented out because we don't use this function currently.
    * It is part of a test to see what is needed to get files to sync
    * properly with Desktop 4.0
    *
   function PadTo4ByteBoundary($string) {
      while ((strlen($string)/2) % 4) {
         $string .= '00';
      }
      return $string;
   }
    *
    */
    
   // Returns the hex encoded header of the pdb file
   // Header = name, attributes, version, creation/modification/backup 
   //          dates, modification number, some offsets, record offsets,
   //          record attributes, appinfo block, sortinfo block
   // Shouldn't be called from outside the class
   function MakeHeader() {
      // 32 bytes = name, but only 31 available (one for null)
      $header = $this->String($this->Name, 31);
      $header = $this->PadString($header, 32);
      
      // Attributes & version fields
      $header .= $this->Int16($this->Attributes);
      $header .= $this->Int16($this->Version);
      
      // Creation, modification, and backup date
      if ($this->CreationTime != 0)
         $header .= $this->Int32($this->CreationTime + PDB_EPOCH_1904);
      else
         $header .= $this->Int32(time() + PDB_EPOCH_1904);
      if ($this->ModificationTime != 0)
         $header .= $this->Int32($this->ModificationTime + PDB_EPOCH_1904);
      else
         $header .= $this->Int32(time() + PDB_EPOCH_1904);
      if ($this->BackupTime != 0)
         $header .= $this->Int32($this->BackupTime + PDB_EPOCH_1904);
      else
         $header .= $this->Int32(0);
      
      // Calculate the initial offset
      $Offset = PDB_HEADER_SIZE + PDB_INDEX_HEADER_SIZE;
      $Offset += PDB_RECORD_HEADER_SIZE * count($this->GetRecordIDs());
      
      // Modification number, app information id, sort information id
      $header .= $this->Int32($this->ModNumber);
      
      $AppInfo_Size = $this->GetAppInfoSize();
      if ($AppInfo_Size > 0) {
         $header .= $this->Int32($Offset);
     $Offset += $AppInfo_Size;
      } else
         $header .= $this->Int32(0);
      
      $SortInfo_Size = $this->GetSortInfoSize();
      if ($SortInfo_Size > 0) {
         $header .= $this->Int32($Offset);
         $Offset += $SortInfo_Size;
      } else
         $header .= $this->Int32(0);
     
      // Type, creator
      $header .= $this->String($this->TypeID, 4);
      $header .= $this->String($this->CreatorID, 4);
      
      // Unique ID seed
      $header .= $this->Int32(0);
      
      // next record list
      $header .= $this->Int32(0);
      
      // Number of records
      $header .= $this->Int16($this->GetRecordCount());
      
      // Compensate for the extra 2 NULL characters in the $Offset
      $Offset += 2;
      
      // Dump each record
      if ($this->GetRecordCount() != 0) {
         $keys = $this->GetRecordIDs();
     sort($keys, SORT_NUMERIC);
     foreach ($keys as $index) {
        $header .= $this->Int32($Offset);
        if (isset($this->RecordAttrs[$index]))
           $header .= $this->Int8($this->RecordAttrs[$index]);
        else
           $header .= $this->Int8(0);
        
        // The unique id is just going to be the record number
        $header .= $this->Int24($index);
        
        $Offset += $this->GetRecordSize($index);
        // *new* method 3
        //$Mod4 = $Offset % 4;
        //if ($Mod4)
        //   $Offset += 4 - $Mod4;
     }
      }
      
      // These are the mysterious two NULL characters that we need
      $header .= $this->Int16(0);
      
      // AppInfo and SortInfo blocks go here
      if ($AppInfo_Size > 0)
         // *new* method 1
         $header .= $this->GetAppInfo();
         //$header .= $this->PadTo4ByteBoundary($this->GetAppInfo());
      
      if ($SortInfo_Size > 0)
         // *new* method 2
         $header .= $this->GetSortInfo();
         //$header .= $this->PadTo4ByteBoundary($this->GetSortInfo());

      return $header;
   }
   
   
   // Writes the database to the file handle specified.
   // Use this function like this:
   //   $file = fopen("output.pdb", "wb"); 
   //   // "wb" = write binary for non-Unix systems
   //   if (! $file) {
   //      echo "big problem -- can't open file";
   //      exit;
   //   }
   //   $pdb->WriteToFile($file);
   //   fclose($file);
   function WriteToFile($file) {
      $header = $this->MakeHeader();
      fwrite($file, pack('H*', $header), strlen($header) / 2);
      $keys = $this->GetRecordIDs();
      sort($keys, SORT_NUMERIC);
      foreach ($keys as $index) {
         // *new* method 3
         //$data = $this->PadTo4ByteBoundary($this->GetRecord($index));
         $data = $this->GetRecord($index);
     fwrite($file, pack('H*', $data), strlen($data) / 2);
      }
      fflush($file);
   }
   
   
   // Writes the database to the standard output (like echo).
   // Can be trapped with output buffering
   function WriteToStdout() {
      // You'd think these three lines would work.
      // If someone can figure out why they don't, please tell me.
      //
      // $fp = fopen('php://stdout', 'wb');
      // $this->WriteToFile($fp);
      // fclose($fp);
      
      $header = $this->MakeHeader();
      echo pack("H*", $header);
      $keys = $this->GetRecordIDs();
      sort($keys, SORT_NUMERIC);
      foreach ($keys as $index) {
         // *new* method 3
     $data = $this->GetRecord($index);
         //$data = $this->PadTo4ByteBoundary($this->GetRecord($index));
     echo pack("H*", $data);
      }
   }
   
   
   // Writes the database to the standard output (like echo) but also
   // writes some headers so that the browser should prompt to save the
   // file properly.
   //
   // Use this only if you didn't send any content and you only want the
   // PHP script to output the PDB file and nothing else.  An example
   // would be if you wanted to have 'download' link so the user can
   // stick the information they are currently viewing and transfer
   // it easily into their handheld.
   //
   // $filename is the desired filename to download the database as.
   // For example, DownloadPDB('memos.pdb');
   function DownloadPDB($filename)
   {
      // Alter the filename to only allow certain characters.
      // Some platforms and some browsers don't respond well if
      // there are illegal characters (such as spaces) in the name of
      // the file being downloaded.
      $filename = preg_replace('/[^-a-zA-Z0-9\\.]/', '_', $filename);
      
      if (strstr($_SERVER['HTTP_USER_AGENT'], 'compatible; MSIE ') !== false &&
          strstr($_SERVER['HTTP_USER_AGENT'], 'Opera') === false) {
     // IE doesn't properly download attachments.  This should work
     // pretty well for IE 5.5 SP 1
     header("Content-Disposition: inline; filename=$filename");
     header("Content-Type: application/download; name=\"$filename\"");
      } else {
         // Use standard headers for Netscape, Opera, etc.
     header("Content-Disposition: attachment; filename=\"$filename\"");
     header("Content-Type: application/x-pilot; name=\"$filename\"");
      }
      
      $this->WriteToStdout();
   }
   
   
   /*
    * Loading in a database
    */
       
   // Reads data from the file and tries to load it properly
   // $file is the already-opened file handle.
   // Returns false if no error
   function ReadFile($file) {
      // 32 bytes = name, but only 31 available
      $this->Name = fread($file, 32);
      
      $i = 0;
      while ($i < 32 && $this->Name[$i] != "\0")
         $i ++;
      $this->Name = substr($this->Name, 0, $i);
      
      $this->Attributes = $this->LoadInt16($file);
      $this->Version = $this->LoadInt16($file);
      
      $this->CreationTime = $this->LoadInt32($file);
      if ($this->CreationTime != 0)
         $this->CreationTime -= PDB_EPOCH_1904;
      if ($this->CreationTime < 0)
         $this->CreationTime = 0;
        
      $this->ModificationTime = $this->LoadInt32($file);
      if ($this->ModificationTime != 0)
         $this->ModificationTime -= PDB_EPOCH_1904;
      if ($this->ModificationTime < 0)
         $this->ModificationTime = 0;
        
      $this->BackupTime = $this->LoadInt32($file);
      if ($this->BackupTime != 0)
         $this->BackupTime -= PDB_EPOCH_1904;
      if ($this->BackupTime < 0)
         $this->BackupTime = 0;

      // Modification number
      $this->ModNumber = $this->LoadInt32($file);
      
      // AppInfo and SortInfo size
      $AppInfoOffset = $this->LoadInt32($file);
      $SortInfoOffset = $this->LoadInt32($file);
      
      // Type, creator
      $this->TypeID = fread($file, 4);
      $this->CreatorID = fread($file, 4);
      
      // Skip unique ID seed
      fread($file, 4);
      
      // skip next record list (hope that's ok)
      fread($file, 4);
      
      $RecCount = $this->LoadInt16($file);
      
      $RecordData = array();
      
      while ($RecCount > 0) {
         $RecCount --;
     $Offset = $this->LoadInt32($file);
     $Attrs = $this->LoadInt8($file);
     $UID = $this->LoadInt24($file);
     $RecordData[] = array('Offset' => $Offset, 'Attrs' => $Attrs,
                           'UID' => $UID);
      }
      
      // Create the offset list
      if ($AppInfoOffset != 0)
         $OffsetList[$AppInfoOffset] = 'AppInfo';
      if ($SortInfoOffset != 0)
         $OffsetList[$SortInfoOffset] = 'SortInfo';
      foreach ($RecordData as $data)
         $OffsetList[$data['Offset']] = array('Record', $data);
      fseek($file, 0, SEEK_END);
      $OffsetList[ftell($file)] = 'EOF';
      
      // Parse each chunk
      ksort($OffsetList);
      $Offsets = array_keys($OffsetList);
      while (count($Offsets) > 1) {
         // Don't use the EOF (which should be the last offset)
     $ThisOffset = $Offsets[0];
     $NextOffset = $Offsets[1];
     if ($OffsetList[$ThisOffset] == 'EOF')
        // Messed up file.  Stop here.
        return true;
     $FuncName = 'Load';
     if (is_array($OffsetList[$ThisOffset])) {
        $FuncName .= $OffsetList[$ThisOffset][0];
        $extraData = $OffsetList[$ThisOffset][1];
     } else {
        $FuncName .= $OffsetList[$ThisOffset];
        $extraData = false;
     }
     fseek($file, $ThisOffset);
     $fileData = fread($file, $NextOffset - $ThisOffset);
     if ($this->$FuncName($fileData, $extraData))
        return -2;
     array_shift($Offsets);
      }
      
      return false;
   }

  
   // Generic function to load the AppInfo block into $this->AppInfo
   // Should only be called within this class
   // Return false to signal no error
   function LoadAppInfo($fileData) {
      $this->AppInfo = bin2hex($fileData);
      return false;
   }
   
   
   // Generic function to load the SortInfo block into $this->SortInfo
   // Should only be called within this class
   // Return false to signal no error
   function LoadSortInfo($fileData) {
      $this->SortInfo = bin2hex($fileData);
      return false;
   }
   
   
   // Generic function to load a record
   // Should only be called within this class
   // Return false to signal no error
   function LoadRecord($fileData, $recordInfo) {
      $this->Records[$recordInfo['UID']] = bin2hex($fileData);
      $this->RecordAttrs[$recordInfo['UID']] = $recordInfo['Attrs'];
      return false;
   }

}

--- NEW FILE: pdb.php ---
<?php

/** We rely on the Data_palm:: abstract class. */
require_once dirname(__FILE__) . '/palm.php';

/**
 * Class to allow data exchange between the Horde applications and
 * palm pdb datebok file.
 *
 * $Horde: framework/Data/Data/pdb.php,v 1.7 2004/02/15 03:50:06 chuck Exp $
 *
 * TODO: export method
 *
 * @author  Mathieu Clabaut <mathieu.clabaut at free.fr>
 * @package Horde_Data
 */
class Data_pdb extends Data_palm {

    var $_extension = 'pdb';
    var $_pdb;

    function importFile($filename)
    {
        $this->_pdb = &new PalmDatebook();
        $fp = fopen($filename, 'r');
        if ($fp) {
            $this->_pdb->ReadFile($fp);
            fclose($fp);
            return $this->import();
        } else {
            return false;
        }
    }

    function importData()
    {
        $nbrec = $this->_pdb->GetRecordCount();
        $data = array();

        foreach ($this->_pdb->GetRecordIDs() as $id) {
            $row = array();
            $record = $this->_pdb->GetRecordRaw($id);
            $row['title'] = $record['Description'];
            $row['category'] = 'Palm';
            if (!empty($record['Note'])) {
                $row['description'] = $record['Note'];
            }
            $row['start_date'] = $record['Date'];
            $row['end_date'] = $row['start_date'];
            if (!empty($record['StartTime'])) {
                $row['start_time'] = $record['StartTime'] . ":00";
                if (!empty($record['EndTime'])) {
                    $row['end_time'] = $record['EndTime'] . ":00" ;
                }
            } else {
                $row['start_time'] = "00:00:00";
                $row['end_time'] = "00:00:00";
                $date = explode('-', $row['start_date']);
                $date[2] = 1 + $date[2];
                $row['end_date'] = implode('-', $date);
            }
            if (!empty($record['Alarm']) &&
                preg_match('/(\d+)([dmh])/', $record['Alarm'], $matches)) {
                switch ($matches[2]) {
                case 'm':
                    $row['alarm'] = $matches[1];
                    break;

                case 'h':
                    $row['alarm'] = $matches[1]*60;
                    break;

                case 'd':
                    $row['alarm'] = $matches[1]*60*24;
                    break;
                }
            }

            if (!empty($record['Repeat'])) {
                switch ($record['Repeat']['Type']) {
                case PDB_DATEBOOK_REPEAT_NONE:
                    $row['recur_type'] = KRONOLITH_RECUR_NONE;
                    break;

                case PDB_DATEBOOK_REPEAT_DAILY:
                    $row['recur_type'] = KRONOLITH_RECUR_DAILY;
                    if (!empty($record['Repeat']['Date'])) {
                        $row['recur_end_date'] = $record['Repeat']['Date'];
                    }
                    $row['recur_interval'] = $record['Repeat']['Frequency'];
                    break;

                case PDB_DATEBOOK_REPEAT_WEEKLY:
                    $row['recur_type'] = KRONOLITH_RECUR_WEEKLY;
                    if (!empty($record['Repeat']['Date'])) {
                        $row['recur_end_date'] = $record['Repeat']['Date'];
                    }
                    $row['recur_interval'] = $record['Repeat']['Frequency'];
                    if (!empty($record['Repeat']['Days'])) {
                        $days=0;
                        for ($c = 0; $c < strlen($record['Repeat']['Days']); $c++) {
                            $days = $days | (int)pow(2, (int)$record['Repeat']['Days']{$c});
                        }
                        $row['recur_data'] = $days;
                    }
                    break;

                case PDB_DATEBOOK_REPEAT_MONTH_BY_DAY:
                    // TODO : to be completed with
                    // $record['Repeat']['WeekNum'] and
                    // $record['Repeat']['DayNum'].
                    $row['recur_type'] = KRONOLITH_RECUR_DAY_OF_MONTH;
                    if (!empty($record['Repeat']['Date'])) {
                        $row['recur_end_date'] = $record['Repeat']['Date'];
                    }
                    $row['recur_interval'] = $record['Repeat']['Frequency'];
                    break;

                case PDB_DATEBOOK_REPEAT_MONTH_BY_DATE:
                    // TODO, verify the compliance with lib/Kronolith.php
                    $row['recur_type'] = KRONOLITH_RECUR_WEEK_OF_MONTH;
                    if (!empty($record['Repeat']['Date'])) {
                        $row['recur_end_date'] = $record['Repeat']['Date'];
                    }
                    $row['recur_interval'] = $record['Repeat']['Frequency'];
                    break;

                case PDB_DATEBOOK_REPEAT_YEARLY:
                    $row['recur_type'] = KRONOLITH_RECUR_YEARLY;
                    if (!empty($record['Repeat']['Date'])) {
                        $row['recur_end_date'] = $record['Repeat']['Date'];
                    }
                    $row['recur_interval'] = $record['Repeat']['Frequency'];
                    break;
                }
            }
            $data[] = $row;
        }

        return $data;
    }

}

/**
 * Class extender for PalmOS Datebook files
 *
 * Copyright (C) 2001 - PHP-PDB development team
 * Licensed under the GNU LGPL
 * See the doc/LEGAL file for more information
 * See http://php-pdb.sourceforge.net/ for more information about the library
 */

// Repeat types
define('PDB_DATEBOOK_REPEAT_NONE', 0);
define('PDB_DATEBOOK_REPEAT_DAILY', 1);
define('PDB_DATEBOOK_REPEAT_WEEKLY', 2);
define('PDB_DATEBOOK_REPEAT_MONTH_BY_DAY', 3);
define('PDB_DATEBOOK_REPEAT_MONTH_BY_DATE', 4);
define('PDB_DATEBOOK_REPEAT_YEARLY', 5);


// Record flags
define('PDB_DATEBOOK_FLAG_DESCRIPTION', 1024); // Record has description
                                            // (mandatory, as far as I know)
define('PDB_DATEBOOK_FLAG_EXCEPTIONS', 2048); // Are there any exceptions?
define('PDB_DATEBOOK_FLAG_NOTE', 4096); // Is there an associated note?
define('PDB_DATEBOOK_FLAG_REPEAT', 8192); // Does the event repeat?
define('PDB_DATEBOOK_FLAG_ALARM', 16384); // Is there an alarm set?
define('PDB_DATEBOOK_FLAG_WHEN', 32768); // Was the 'when' updated?
                                         // (Internal use only?)


/* The data for SetRecordRaw and from GetRecordRaw should be/return a
 * special array, detailed below.  Optional values can be set to '' or not
 * defined.  If they are anything else (including zero), they are considered
 * to be 'set'.  Optional values are marked with a ^.
 *
 * Key           Example          Description
 * ------------------------------------------
 * StartTime     2:00             Starting time of event, 24 hour format
 * EndTime       13:00            Ending time of event, 24 hour format
 * Date          2001-01-23       Year-Month-Day of event
 * Description   Title            This is the title of the event
 * Alarm         5d               ^ Number of units (m=min, h=hours, d=days)
 * Repeat        ???              ^ Repeating event data (array)
 * Note          NoteNote         ^ A note for the event
 * Exceptions    ???              ^ Exceptions to the event
 * WhenChanged   ???              ^ True if "when info" for event has changed
 * Flags         3                ^ User flags (highest bit allowed is 512)
 *
 * EndTime must happen at or after StartTime.  The time the event occurs
 * may not pass midnight (0:00).  If the event doesn't have a time, use ''
 * or do not define StartTime and EndTime.
 *
 * Repeating events:
 *
 *    No repeat (or leave the array undefined):
 *       $repeat['Type'] = PDB_DATEBOOK_REPEAT_NONE;
 *
 *    Daily repeating events:
 *       $repeat['Type'] = PDB_DATEBOOK_REPEAT_DAILY;
 *       $repeat['Frequency'] = FREQ;  // Explained below
 *       $repeat['End'] = END_DATE;  // Explained below
 *
 *    Weekly repeating events:
 *       $repeat['Type'] = PDB_DATEBOOK_REPEAT_WEEKLY;
 *       $repeat['Frequency'] = FREQ;  // Explained below
 *       $repeat['Days'] = DAYS; // Explained below
 *       $repeat['End'] = END_DATE;  // Explained below
 *       $repeat['StartOfWeek'] = SOW; // Explained below
 *
 *    "Monthly by day" repeating events:
 *       $repeat['Type'] = PDB_DATEBOOK_REPEAT_MONTH_BY_DAY;
 *       $repeat['WeekNum'] = WEEKNUM;  // Explained below
 *       $repeat['DayNum'] = DAYNUM;  // Explained below
 *       $repeat['Frequency'] = FREQ;  // Explained below
 *       $repeat['End'] = END_DATE;  // Explained below
 *
 *    "Monthly by date" repeating events:
 *       $repeat['Type'] = PDB_DATEBOOK_REPEAT_MONTH_BY_DATE;
 *       $repeat['Frequency'] = FREQ;  // Explained below
 *       $repeat['End'] = END_DATE;  // Explained below
 *
 *    Yearly repeating events:
 *       $repeat['Type'] = PDB_DATEBOOK_REPEAT_YEARLY;
 *       $repeat['Frequency'] = FREQ;  // Explained below
 *       $repeat['End'] = END_DATE;  // Explained below
 *
 *    There is also two mysterious 'unknown' fields for the repeat event that
 *    will be populated if the database is loaded from a file.  They will
 *    otherwise default to 0.  They are 'unknown1' and 'unknown2'.
 *
 *    FREQ = Frequency of the event.  If it is a daily event and you want it
 *           to happen every other day, set Frequency to 2.  This will default
 *           to 1 if not specified.
 *    END_DATE = The last day, month, and year on which the event occurs.
 *               Format is YYYY-MM-DD.  If not specified, no end date will
 *               be set.
 *    DAYS = What days during the week the event occurs.  This is a string of
 *           numbers from 0 - 6.  I'm not sure if 0 = Sunday or if 0 =
 *           start of week from the preferences.
 *    SOW = As quoted from P5-Palm:  "I'm not sure what this is, but the
 *          Datebook app appears to perform some hairy calculations
 *          involving this."
 *    WEEKNUM = The number of the week on which the event occurs.  5 is the
 *              last week of the month.
 *    DAYNUM = The day of the week on which the event occurs.  Again, I don't
 *             know if 0 = Sunday or if 0 = start of week from the prefs.
 *
 * Exceptions are specified in an array consisting of dates the event occured
 * and did not happen or should not be shown.  Dates are in the format
 * YYYY-MM-DD
 *
 * @package Horde_Data
 */
class PalmDatebook extends PalmDB {
   var $FirstDay;


   // Constructor -- initialize the default values
   function PalmDatebook () {
      PalmDB::PalmDB('DATA', 'date', 'DatebookDB');
      $this->FirstDay = 0;
   }


   // Returns an array with default data for a new record.
   // This doesn't actually add the record.
   function NewRecord() {
      // Default event is untimed
      // Event's date is today
      $Event['Date'] = date("Y-m-d");

      // Set an alarm 10 min before the event
      $Event['Alarm'] = '10m';

      return $Event;
   }


   // Converts a date string ( YYYY-MM-DD )( "2001-10-31" )
   // into bitwise ( YYYY YYYM MMMD DDDD )
   // Should only be used when saving
   function DateToInt16($date) {
      $YMD = explode('-', $date);
      return ($YMD[0] - 1904) * 512 + $YMD[1] * 32 + $YMD[2];
   }


   // Converts a bitwise date ( YYYY YYYM MMMD DDDD )
   // Into the human readable date string ( YYYY-MM-DD )( "2001-10-31" )
   // Should only be used when loading
   function Int16ToDate($number) {
      $year = $number / 512;
      settype($year, "integer");
      $year += 1904;
      $number = $number % 512;
      $month = $number / 32;
      settype($month, "integer");
      $day = $number % 32;
      return $year . '-' . $month . '-' . $day;
   }


   // Prepares the record flags for the specified record;
   // Should only be used when saving
   function GetRecordFlags(& $data) {
      if (! isset($data['Flags']))
         $data['Flags'] = 0;
      $Flags = $data['Flags'] % 1024;
      if (isset($data['Description']) && $data['Description'] != '')
         $Flags += PDB_DATEBOOK_FLAG_DESCRIPTION;
      if (isset($data['Exceptions']) && is_array($data['Exceptions']) &&
          count($data['Exceptions']) > 0)
     $Flags += PDB_DATEBOOK_FLAG_EXCEPTIONS;
      if (isset($data['Note']) && $data['Note'] != '')
         $Flags += PDB_DATEBOOK_FLAG_NOTE;
      if (isset($data['Repeat']) && is_array($data['Repeat']) &&
          count($data['Repeat']) > 0)
     $Flags += PDB_DATEBOOK_FLAG_REPEAT;
      if (isset($data['Alarm']) && $data['Alarm'] != '' &&
          preg_match('/^([0-9]+)([mMhHdD])$/', $data['Alarm'], $AlarmMatch))
         $Flags += PDB_DATEBOOK_FLAG_ALARM;
      if (isset($data['WhenChanged']) && $data['WhenChanged'] != '' &&
          $data['WhenChanged'])
     $Flags += PDB_DATEBOOK_FLAG_WHEN;

      $data['Flags'] = $Flags;
   }


   // Overrides the GetRecordSize method.
   // Probably should only be used when saving
   function GetRecordSize($num = false) {
      if ($num === false)
         $num = $this->CurrentRecord;

      if (! isset($this->Records[$num]) || ! is_array($this->Records[$num]))
         return PalmDB::GetRecordSize($num);

      $data = $this->Records[$num];

      // Start Time and End Time (4)
      // The day of the event (2)
      // Flags (2)
      $Bytes = 8;
      $this->GetRecordFlags($data);

      if ($data['Flags'] & PDB_DATEBOOK_FLAG_ALARM)
         $Bytes += 2;

      if ($data['Flags'] & PDB_DATEBOOK_FLAG_REPEAT)
     $Bytes += 8;

      if ($data['Flags'] & PDB_DATEBOOK_FLAG_EXCEPTIONS)
         $Bytes += 2 + count($data['Exceptions']) * 2;

      if ($data['Flags'] & PDB_DATEBOOK_FLAG_DESCRIPTION)
         $Bytes += strlen($data['Description']) + 1;

      if ($data['Flags'] & PDB_DATEBOOK_FLAG_NOTE)
         $Bytes += strlen($data['Note']) + 1;

      return $Bytes;
   }


   // Overrides the GetRecord method.  We store data in associative arrays.
   // Just convert the data into the proper format and then return the
   // generated string.
   function GetRecord($num = false) {
      if ($num === false)
         $num = $this->CurrentRecord;

      if (! isset($this->Records[$num]) || ! is_array($this->Records[$num]))
         return PalmDB::GetRecord($num);

      $data = $this->Records[$num];
      $RecordString = '';


      // Start Time and End Time
      // 4 bytes
      // 0xFFFFFFFF if the event has no time
      if (! isset($data['StartTime']) || ! isset($data['EndTime']) ||
          strpos($data['StartTime'], ':') === false ||
      strpos($data['EndTime'], ':') === false) {
     $RecordString .= $this->Int16(65535);
     $RecordString .= $this->Int16(65535);
      } else {
         list($StartH, $StartM) = explode(':', $data['StartTime']);
         list($EndH, $EndM) = explode(':', $data['EndTime']);
     if ($StartH < 0 || $StartH > 23 || $StartM < 0 || $StartM > 59 ||
         $EndH < 0 || $EndH > 23 || $EndM < 0 || $EndM > 59) {
        $RecordString .= $this->Int16(65535);
        $RecordString .= $this->Int16(65535);
     } else {
        if ($EndH < $StartH || ($EndH == $StartH && $EndM < $StartM)) {
           $EndM = $StartM;
           if ($StartH < 23)
              $EndH = $StartH + 1;
           else
              $EndH = $StartH;
        }
        $RecordString .= $this->Int8($StartH);
        $RecordString .= $this->Int8($StartM);
        $RecordString .= $this->Int8($EndH);
        $RecordString .= $this->Int8($EndM);
     }
      }

      // The day of the event
      // For repeating events, this is the first day the event occurs
      $RecordString .= $this->Int16($this->DateToInt16($data['Date']));

      // Flags
      $this->GetRecordFlags($data);
      $Flags = $data['Flags'];
      $RecordString .= $this->Int16($Flags);

      if ($Flags & PDB_DATEBOOK_FLAG_ALARM &&
          preg_match('/^([0-9]+)([mMhHdD])$/', $data['Alarm'], $AlarmMatch)) {
         $RecordString .= $this->Int8($AlarmMatch[1]);
     $AlarmMatch[2] = strtolower($AlarmMatch[2]);
     if ($AlarmMatch[2] == 'm')
        $RecordString .= $this->Int8(0);
     elseif ($AlarmMatch[2] == 'h')
        $RecordString .= $this->Int8(1);
     else
        $RecordString .= $this->Int8(2);
      }

      if ($Flags & PDB_DATEBOOK_FLAG_REPEAT) {
         $d = $data['Repeat'];

         $RecordString .= $this->Int8($d['Type']);

     if (! isset($d['unknown1']))
        $d['unknown1'] = 0;
     $RecordString .= $this->Int8($d['unknown1']);

     if (isset($d['End']))
        $RecordString .= $this->Int16($this->DateToInt16($d['End']));
     else
        $RecordString .= $this->Int16(65535);

     if (! isset($d['Frequency']))
        $d['Frequency'] = 1;
     $RecordString .= $this->Int8($d['Frequency']);

     if ($d['Type'] == PDB_DATEBOOK_REPEAT_WEEKLY) {
        $days = $d['Days'];
        $flags = 0;
        $QuickLookup = array(1, 2, 4, 8, 16, 32, 64);
        $i = 0;
        while ($i < strlen($days)) {
           $num = $days[$i];
           settype($num, 'integer');
           if (isset($QuickLookup[$num]))
              $flags = $flags | $QuickLookup[$num];
           $i ++;
        }
        $RecordString .= $this->Int8($flags);
        if (isset($d['StartOfWeek']) && $d['StartOfWeek'] != '')
           $RecordString .= $this->Int8($d['StartOfWeek']);
        else
           $RecordString .= $this->Int8(0);
     } elseif ($d['Type'] == PDB_DATEBOOK_REPEAT_MONTH_BY_DAY) {
        if ($d['WeekNum'] > 5)
           $d['WeekNum'] = 5;
        $RecordString .= $this->Int8($d['WeekNum'] * 7 + $d['DayNum']);
        $RecordString .= $this->Int8(0);
     } else {
        $RecordString .= $this->Int16(0);
     }
     if (! isset($d['unknown2']))
        $d['unknown2'] = 0;
     $RecordString .= $this->Int8($d['unknown2']);
      }

      if ($Flags & PDB_DATEBOOK_FLAG_EXCEPTIONS) {
         $d = $data['Exceptions'];
         $RecordString .= $this->Int16(count($d));
     foreach ($d as $exception) {
        $RecordString .= $this->Int16($this->DateToInt16($exception));
     }
      }

      if ($Flags & PDB_DATEBOOK_FLAG_DESCRIPTION) {
         $RecordString .= $this->String($data['Description']);
         $RecordString .= $this->Int8(0);
      }

      if ($Flags & PDB_DATEBOOK_FLAG_NOTE) {
         $RecordString .= $this->String($data['Note']);
     $RecordString .= $this->Int8(0);
      }

      return $RecordString;
   }


   // Returns the size of the AppInfo block.  It is the size of the
   // category list plus four bytes.
   function GetAppInfoSize() {
      return PDB_CATEGORY_SIZE + 4;
   }


   // Returns the AppInfo block.  It is composed of the category list (which
   // doesn't seem to be used and is just filled with NULL bytes) and four
   // bytes that specify the first day of the week.  Not sure what that
   // value is supposed to be, so I just use zero.
   function GetAppInfo() {
      // Category list (Nulls)
      $this->AppInfo = $this->PadString('', PDB_CATEGORY_SIZE);

      // Unknown thing (first_day_in_week)
      // 00 00 FD 00 == where FD is the first day in week.
      // I'm using 0 as the default value since I don't know what it should be
      $this->AppInfo .= $this->Int16(0);
      $this->AppInfo .= $this->Int8($this->FirstDay);
      $this->AppInfo .= $this->Int8(0);

      return $this->AppInfo;
   }


   // Parses $fileData for the information we need when loading a datebook
   // file
   function LoadAppInfo($fileData) {
      $fileData = substr($fileData, PDB_CATEGORY_SIZE + 2);
      if (strlen($fileData < 1))
         return;
      $this->FirstDay = $this->LoadInt8($fileData);
   }


   // Converts the datebook record data loaded from a file into the internal
   // storage method that is used for the rest of the class and for ease of
   // use.
   // Return false to signal no error
   function LoadRecord($fileData, $RecordInfo) {
      $this->RecordAttrs[$RecordInfo['UID']] = $RecordInfo['Attrs'];

      $NewRec = $this->NewRecord();
      $StartH = $this->LoadInt8(substr($fileData, 0, 1));
      $StartM = $this->LoadInt8(substr($fileData, 1, 1));
      $EndH = $this->LoadInt8(substr($fileData, 2, 1));
      $EndM = $this->LoadInt8(substr($fileData, 3, 1));
      if ($StartH != 255 && $StartM != 255) {
         $NewRec['StartTime'] = $StartH . ':';
     if ($StartM < 10)
        $NewRec['StartTime'] .= '0';
     $NewRec['StartTime'] .= $StartM;
      }
      if ($EndH != 255 && $EndM != 255) {
         $NewRec['EndTime'] = $EndH . ':';
     if ($EndM < 10)
        $NewRec['EndTime'] .= '0';
     $NewRec['EndTime'] .= $EndM;
      }
      $NewRec['Date'] = $this->LoadInt16(substr($fileData, 4, 2));
      $NewRec['Date'] = $this->Int16ToDate($NewRec['Date']);
      $Flags = $this->LoadInt16(substr($fileData, 6, 2));
      $NewRec['Flags'] = $Flags;
      $fileData = substr($fileData, 8);

      if ($Flags & PDB_DATEBOOK_FLAG_WHEN)
         $NewRec['WhenChanged'] = true;

      if ($Flags & PDB_DATEBOOK_FLAG_ALARM) {
         $amount = $this->LoadInt8(substr($fileData, 0, 1));
     $unit = $this->LoadInt8(substr($fileData, 1, 1));
     if ($unit == 0)
        $unit = 'm';
     elseif ($unit == 1)
        $unit = 'h';
     else
        $unit = 'd';
     $NewRec['Alarm'] = $amount . $unit;
     $fileData = substr($fileData, 2);
      } else
         unset($NewRec['Alarm']);

      if ($Flags & PDB_DATEBOOK_FLAG_REPEAT) {
         $Repeat = array();
     $Repeat['Type'] = $this->LoadInt8(substr($fileData, 0, 1));
     $Repeat['unknown1'] = $this->LoadInt8(substr($fileData, 1, 1));
     $End = $this->LoadInt16(substr($fileData, 2, 2));
     $Repeat['Frequency'] = $this->LoadInt8(substr($fileData, 4, 1));
     $RepeatOn = $this->LoadInt8(substr($fileData, 5, 1));
     $RepeatSoW = $this->LoadInt8(substr($fileData, 6, 1));
     $Repeat['unknown2'] = $this->LoadInt8(substr($fileData, 7, 1));
     $fileData = substr($fileData, 8);

     if ($End != 65535 && $End <= 0)
        $Repeat['End'] = $this->Int16ToDate($End);

         if ($Repeat['Type'] == PDB_DATEBOOK_REPEAT_WEEKLY) {
        $days = '';
        if ($RepeatOn & 64)
           $days .= '0';
        if ($RepeatOn & 32)
           $days .= '1';
        if ($RepeatOn & 16)
           $days .= '2';
        if ($RepeatOn & 8)
           $days .= '3';
        if ($RepeatOn & 4)
           $days .= '4';
        if ($RepeatOn & 2)
           $days .= '5';
        if ($RepeatOn & 1)
           $days .= '6';
        $Repeat['Days'] = $days;
        $Repeat['StartOfWeek'] = $RepeatSoW;
     } elseif ($Repeat['Type'] == PDB_DATEBOOK_REPEAT_MONTH_BY_DAY) {
        $Repeat['DayNum'] = $RepeatOn % 7;
        $RepeatOn /= 7;
        settype($RepeatOn, 'integer');
        $Repeat['WeekNum'] = $RepeatOn;
     }

     $NewRec['Repeat'] = $Repeat;
      }

      if ($Flags & PDB_DATEBOOK_FLAG_EXCEPTIONS) {
         $Exceptions = array();
     $number = $this->LoadInt16(substr($fileData, 0, 2));
     $fileData = substr($fileData, 2);
     while ($number --) {
        $Exc = $this->LoadInt16(substr($fileData, 0, 2));
        $Exceptions[] = $this->Int16ToDate($Exc);
        $fileData = substr($fileData, 2);
     }
     $NewRec['Exceptions'] = $Exceptions;
      }

      if ($Flags & PDB_DATEBOOK_FLAG_DESCRIPTION) {
         $i = 0;
     $NewRec['Description'] = '';
     while ($i < strlen($fileData) && $fileData[$i] != "\0") {
        $NewRec['Description'] .= $fileData[$i];
        $i ++;
     }
     $fileData = substr($fileData, $i + 1);
      }

      if ($Flags & PDB_DATEBOOK_FLAG_NOTE) {
         $i = 0;
     $NewRec['Note'] = '';
     while ($i < strlen($fileData) && $fileData[$i] != "\0") {
        $NewRec['Note'] .= $fileData[$i];
        $i ++;
     }
     $fileData = substr($fileData, 0, $i + 1);
      }

      $this->Records[$RecordInfo['UID']] = $NewRec;
      return false;
   }

}

--- NEW FILE: tsv.php ---
<?php
/**
 * Horde_Data implementation for tab-separated data (TSV).
 *
 * $Horde: framework/Data/Data/tsv.php,v 1.22 2004/05/25 15:52:44 jan Exp $
 *
 * Copyright 1999-2004 Jan Schneider <jan at horde.org>
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
 *
 * @author  Jan Schneider <jan at horde.org>
 * @author  Chuck Hagenbuch <chuck at horde.org>
 * @version $Revision: 1.1 $
 * @since   Horde 1.3
 * @package Horde_Data
 */
class Horde_Data_tsv extends Horde_Data {

    var $_extension = 'tsv';
    var $_contentType = 'text/tab-separated-values';

    function importData($contents, $header = false, $delimiter = "\t")
    {
        $contents = explode("\n", $contents);
        $data = array();
        if ($header) {
            $head = explode($delimiter, array_shift($contents));
        }
        foreach ($contents as $line) {
            if (trim($line) == '') {
                continue;
            }
            $line = explode($delimiter, $line);
            if (!isset($head)) {
                $data[] = $line;
            } else {
                $newline = array();
                for ($i = 0; $i < count($head); $i++) {
                    $newline[$head[$i]] = empty($line[$i]) ? '' : $line[$i];
                }
                $data[] = $newline;
            }
        }
        return $data;
    }

    /**
     * Builds a TSV file from a given data structure and returns it as a
     * string.
     *
     * @access public
     * @param array $data        A two-dimensional array containing the data
     *                           set.
     * @param bool $header       If true, the rows of $data are associative
     *                           arrays with field names as their keys.
     * @return string            The TSV data.
     */
    function exportData($data, $header = false)
    {
        if (!is_array($data) || count($data) == 0) {
            return '';
        }
        $export = '';
        if ($header) {
            $export = implode("\t", array_keys(current($data))) . "\n";
        }
        foreach ($data as $row) {
            $export .= implode("\t", $row) . "\n";
        }
        return $export;
    }

    /**
     * Builds a TSV file from a given data structure and triggers its download.
     * It DOES NOT exit the current script but only outputs the correct headers
     * and data.
     *
     * @access public
     * @param string $filename   The name of the file to be downloaded.
     * @param array $data        A two-dimensional array containing the data set.
     * @param bool $header       If true, the rows of $data are associative arrays
     *                           with field names as their keys.
     */
    function exportFile($filename, $data, $header = false)
    {
        $export = $this->exportData($data, $header);
        $GLOBALS['browser']->downloadHeaders($filename, 'text/tab-separated-values', false, strlen($export));
        echo $export;
    }

    /**
     * Takes all necessary actions for the given import step, parameters and
     * form values and returns the next necessary step.
     *
     * @access public
     *
     * @param integer $action        The current step. One of the IMPORT_*
     *                               constants.
     * @param optional array $param  An associative array containing needed
     *                               parameters for the current step.
     *
     * @return mixed  Either the next step as an integer constant or imported
     *                data set after the final step.
     */
    function nextStep($action, $param = array())
    {
        switch ($action) {
        case IMPORT_FILE:
            $next_step = parent::nextStep($action, $param);
            if (is_a($next_step, 'PEAR_Error')) {
                return $next_step;
            }

            if ($_SESSION['import_data']['format'] == 'mulberry' ||
                $_SESSION['import_data']['format'] == 'pine') {
                $_SESSION['import_data']['data'] = $this->importFile($_FILES['import_file']['tmp_name']);
                $format = $_SESSION['import_data']['format'];
                if ($format == 'mulberry') {
                    $appKeys  = array('alias', 'name', 'email', 'company', 'workAddress', 'workPhone', 'homePhone', 'fax', 'notes');
                    $dataKeys = array(0, 1, 2, 3, 4, 5, 6, 7, 9);
                } elseif ($format == 'pine') {
                    $appKeys = array('alias', 'name', 'email', 'notes');
                    $dataKeys = array(0, 1, 2, 4);
                }
                foreach ($appKeys as $key => $app) {
                    $map[$dataKeys[$key]] = $app;
                }
                $data = array();
                foreach ($_SESSION['import_data']['data'] as $row) {
                    $hash = array();
                    if ($format == 'mulberry') {
                        if (preg_match("/^Grp:/", $row[0])) {
                            continue;
                        }
                        $row[1] = preg_replace('/^([^,"]+),\s*(.*)$/', '$2 $1', $row[1]);
                        foreach ($dataKeys as $key) {
                            if (array_key_exists($key, $row)) {
                                $hash[$key] = stripslashes(preg_replace('/\\\\r/', "\n", $row[$key]));
                            }
                        }
                    } elseif ($format = 'pine') {
                        if (count($row) < 3 || preg_match("/^#DELETED/", $row[0]) || preg_match("/[()]/", $row[2])) {
                            continue;
                        }
                        $row[1] = preg_replace('/^([^,"]+),\s*(.*)$/', '$2 $1', $row[1]);
                        foreach ($dataKeys as $key) {
                            if (array_key_exists($key, $row)) {
                                $hash[$key] = $row[$key];
                            }
                        }
                    }
                    $data[] = $hash;
                }
                $_SESSION['import_data']['data'] = $data;
                $_SESSION['import_data']['map'] = $map;
                $ret = $this->nextStep(IMPORT_DATA, $param);
                return $ret;
            }

            /* Move uploaded file so that we can read it again in the next step
               after the user gave some format details. */
            $uploaded = Browser::wasFileUploaded('import_file', _("TSV file"));
            if (is_a($uploaded, 'PEAR_Error')) {
                return PEAR::raiseError($uploaded->getMessage());
            }
            $file_name = Horde::getTempFile('import', false);
            if (!move_uploaded_file($_FILES['import_file']['tmp_name'], $file_name)) {
                return PEAR::raiseError(_("The uploaded file could not be saved."));
            }
            $_SESSION['import_data']['file_name'] = $file_name;

            /* Read the file's first two lines to show them to the user. */
            $_SESSION['import_data']['first_lines'] = '';
            $fp = @fopen($file_name, 'r');
            if ($fp) {
                $line_no = 1;
                while ($line_no < 3 && $line = fgets($fp)) {
                    $newline = String::length($line) > 100 ? "\n" : '';
                    $_SESSION['import_data']['first_lines'] .= substr($line, 0, 100) . $newline;
                    $line_no++;
                }
            }
            return IMPORT_TSV;
            break;

        case IMPORT_TSV:
            $_SESSION['import_data']['header'] = Util::getFormData('header');
            $import_data = $this->importFile($_SESSION['import_data']['file_name'],
                                             $_SESSION['import_data']['header']);
            $_SESSION['import_data']['data'] = $import_data;
            unset($_SESSION['import_data']['map']);
            return IMPORT_MAPPED;
            break;

        default:
            return parent::nextStep($action, $param);
            break;
        }
    }

}

--- NEW FILE: vcard.php ---
<?php

/** We rely on the Horde_Data_imc:: abstract class. */
require_once dirname(__FILE__) . '/imc.php';

// The following were shamelessly yoinked from Contact_Vcard_Build
// Part numbers for N components.
define('VCARD_N_FAMILY',     0);
define('VCARD_N_GIVEN',      1);
define('VCARD_N_ADDL',       2);
define('VCARD_N_PREFIX',     3);
define('VCARD_N_SUFFIX',     4);

// Part numbers for ADR components.
define('VCARD_ADR_POB',      0);
define('VCARD_ADR_EXTEND',   1);
define('VCARD_ADR_STREET',   2);
define('VCARD_ADR_LOCALITY', 3);
define('VCARD_ADR_REGION',   4);
define('VCARD_ADR_POSTCODE', 5);
define('VCARD_ADR_COUNTRY',  6);

// Part numbers for GEO components.
define('VCARD_GEO_LAT',      0);
define('VCARD_GEO_LON',      1);

/**
 * Implement the Horde_Data:: API for vCard data.
 *
 * $Horde: framework/Data/Data/vcard.php,v 1.31 2004/05/10 13:41:07 jan Exp $
 *
 * Copyright 1999-2004 Jan Schneider <jan at horde.org>
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
 *
 * @author  Jan Schneider <jan at horde.org>
 * @version $Horde: framework/Data/Data/vcard.php,v 1.31 2004/05/10 13:41:07 jan Exp $
 * @since   Horde 3.0
 * @package Horde_Data
 */
class Horde_Data_vcard extends Horde_Data_imc {

    /**
     * The vCard version.
     *
     * @access private
     * @var string $_version
     */
    var $_version;

    function importData($text)
    {
        $res = parent::importData($text);
        if (is_a($res, 'PEAR_Error')) {
            return $res;
        }
        return $this->_objects;
    }

    /**
     * Builds a vCard file from a given data structure and returns it
     * as a string.
     *
     * @access public
     *
     * @param array $data  A two-dimensional array containing the data set.
     *
     * @return string  The vCard data.
     */
    function exportData($data)
    {
        /* According to RFC 2425, we should always use CRLF-terminated
           lines. */
        $newline = "\r\n";

        $file = "BEGIN:VCARD${newline}VERSION:3.0${newline}";
        foreach ($data as $key => $val) {
            if (!empty($val)) {
                // Basic encoding. Newlines for now; more work here to
                // make this RFC-compliant.
                $file .= $key . ':' .  $this->_quoteAndFold($val);
            }
        }
        $file .= "END:VCARD$newline";

        return $file;
    }

    /**
     * Builds a vCard file from a given data structure and triggers
     * its download. It DOES NOT exit the current script but only
     * outputs the correct headers and data.
     *
     * @access public
     *
     * @param string $filename   The name of the file to be downloaded.
     * @param array  $data       A two-dimensional array containing the data set.
     */
    function exportFile($filename, $data, $charset = null)
    {
        $export = $this->exportData($data);
        $cType = 'text/x-vcard';
        if (!empty($charset)) {
            $cType .= '; charset="' . $charset . '"';
        }
        $GLOBALS['browser']->downloadHeaders($filename, $cType, false, strlen($export));
        echo $export;
    }

    function _build($data, &$i)
    {
        $objects = array();

        while (isset($data[$i])) {
            if (String::upper($data[$i]['name']) != 'BEGIN') {
                return PEAR::raiseError(sprintf(_("Import Error: Expected \"BEGIN\" on line %d."), $i));
            }
            $type = String::upper($data[$i]['values'][0]);
            $object = array('type' => $type);
            $object['objects'] = array();
            $object['params'] = array();
            $i++;
            while (isset($data[$i]) && String::upper($data[$i]['name']) != 'END') {
                if (String::upper($data[$i]['name']) == 'BEGIN') {
                    $object['objects'][] = $this->_build($data, $i);
                } else {
                    $object['params'][] = $data[$i];
                    if (String::upper($data[$i]['name']) == 'VERSION') {
                        $object['version'] = $data[$i]['values'][0];
                    }
                }
                $i++;
            }
            if (!isset($data[$i])) {
                return PEAR::raiseError(_("Import Error: Unexpected end of file."));
            }
            if (String::upper($data[$i]['values'][0]) != $type) {
                return PEAR::raiseError(sprintf(_("Import Error: Type mismatch. Expected \"END:%s\" on line %d."), $type, $i));
            }
            $objects[] = $object;
            $i++;
        }

        $this->_objects = $objects;
    }

    function read($attribute, $index = 0)
    {
        if (($index == 0) && ($this->_version < 3.0)) {
            $value = $attribute['value21'];
        } else {
            $value = $attribute['values'][$index];
        }

        if (isset($attribute['params']['ENCODING'])) {
            switch ($attribute['params']['ENCODING'][0]) {
            case 'QUOTED-PRINTABLE':
                $value = quoted_printable_decode($value);
                break;
            }
        }

        return $value;
    }

    function getValues($attribute, $card = 0)
    {
        $values = array();
        $attribute = String::upper($attribute);

        if (isset($this->_objects[$card])) {
            for ($i = 0; $i < count($this->_objects[$card]['params']); $i++) {
                $param = $this->_objects[$card]['params'][$i];
                if (String::upper($param['name']) == $attribute) {
                    for ($j = 0; $j < count($param['values']); $j++) {
                        $values[] = array('value' => $this->read($param, $j), 'params' => $param['params']);
                    }
                }
            }
        }

        return $values;
    }

    function getBareEmail($address)
    {
        require_once 'Mail/RFC822.php';
        require_once 'Horde/MIME.php';

        static $rfc822;
        if (is_null($rfc822)) {
            $rfc822 = &new Mail_RFC822();
        }

        $rfc822->validateMailbox($address);

        return MIME::rfc822WriteAddress($address->mailbox, $address->host);
    }

    /**
     * Return a data hash from a vCard object.
     *
     * @param array $card  The card to convert.
     *
     * @return array  The hashed data.
     *
     * @since Horde 3.0
     */
    function toHash($card)
    {
        $this->_version = isset($card['version']) ? $card['version'] : null;
        $hash = array();
        foreach ($card['params'] as $item) {
            switch ($item['name']) {
            case 'FN':
                $hash['name'] = $this->read($item);
                break;

            case 'N':
                $name = explode(';', $this->read($item));
                $hash['lastname'] = $name[VCARD_N_FAMILY];
                $hash['firstname'] = $name[VCARD_N_GIVEN];
                break;

            case 'NICKNAME':
                $hash['nickname'] = $this->read($item);
                break;

            // We use LABEL but also support ADR.
            case 'LABEL':
                if (isset($item['params']['HOME'])) {
                    $hash['homeAddress'] = $this->read($item);
                } elseif (isset($item['params']['WORK'])) {
                    $hash['workAddress'] = $this->read($item);
                } else {
                    $hash['workAddress'] = $this->read($item);
                }
                break;

            // for vCard 3.0
            case 'ADR':
                if (isset($item['params']['TYPE'])) {
                    foreach ($item['params']['TYPE'] as $adr) {
                        if (String::upper($adr) == 'HOME') {
                            $address = explode(';', $this->read($item));
                            $hash['homeAddress'] = $address[VCARD_ADR_STREET];
                            $hash['homeCity'] = $address[VCARD_ADR_LOCALITY];
                            $hash['homeProvince'] = $address[VCARD_ADR_REGION];
                            $hash['homePostalCode'] = $address[VCARD_ADR_POSTCODE];
                            $hash['homeCountry'] = $address[VCARD_ADR_COUNTRY];
                        } elseif (String::upper($adr) == 'WORK') {
                            $address = explode(';', $this->read($item));
                            $hash['workAddress'] = $address[VCARD_ADR_STREET];
                            $hash['workCity'] = $address[VCARD_ADR_LOCALITY];
                            $hash['workProvince'] = $address[VCARD_ADR_REGION];
                            $hash['workPostalCode'] = $address[VCARD_ADR_POSTCODE];
                            $hash['workCountry'] = $address[VCARD_ADR_COUNTRY];
                        }
                    }
                }
                break;

            case 'TEL':
                if (isset($item['params']['VOICE'])) {
                    if (isset($item['params']['HOME'])) {
                        $hash['homePhone'] = $this->read($item);
                    } elseif (isset($item['params']['WORK'])) {
                        $hash['workPhone'] = $this->read($item);
                    } elseif (isset($item['params']['CELL'])) {
                        $hash['cellPhone'] = $this->read($item);
                    }
                } elseif (isset($item['params']['FAX'])) {
                    $hash['fax'] = $this->read($item);
                } elseif (isset($item['params']['TYPE'])) {
                    // for vCard 3.0
                    foreach ($item['params']['TYPE'] as $tel) {
                        if (String::upper($tel) == 'WORK') {
                            $hash['workPhone'] = $this->read($item);
                        } elseif (String::upper($tel) == 'HOME') {
                            $hash['homePhone'] = $this->read($item);
                        } elseif (String::upper($tel) == 'CELL') {
                            $hash['cellPhone'] = $this->read($item);
                        } elseif (String::upper($tel) == 'FAX') {
                            $hash['fax'] = $this->read($item);
                        }
                    }
                }
                break;

            case 'EMAIL':
                if (isset($item['params']['PREF']) || !isset($hash['email'])) {
                    $hash['email'] = $this->getBareEmail($this->read($item));
                }
                break;

            case 'TITLE':
                $hash['title'] = $this->read($item);
                break;

            case 'ORG':
                $units = array();
                for ($i = 0; $i < count($item['values']); $i++) {
                    $units[] = $this->read($item, $i);
                }
                $hash['company'] = implode(', ', $units);
                break;

            case 'NOTE':
                $hash['notes'] = $this->read($item);
                break;

            case 'URL':
                $hash['website'] = $this->read($item);
                break;
            }
        }

        return $hash;
    }

    /**
     * Return an array of vCard properties -> values from a hash of
     * attributes.
     *
     * @param array $hash  The hash of values to convert.
     *
     * @return array  The vCard format data.
     *
     * @since Horde 3.0
     */
    function fromHash($hash)
    {
        $card = array();
        foreach ($hash as $key => $val) {
            switch ($key) {
            case 'name':
                $card['FN'] = $val;
                break;

            case 'nickname':
                $card['NICKNAME'] = $val;
                break;

            case 'homePhone':
                $card['TEL;TYPE=HOME'] = $val;
                break;

            case 'workPhone':
                $card['TEL;TYPE=WORK'] = $val;
                break;

            case 'cellPhone':
                $card['TEL;TYPE=CELL'] = $val;
                break;

            case 'fax':
                $card['TEL;TYPE=FAX'] = $val;
                break;

            case 'email':
                $card['EMAIL'] = $this->getBareEmail($val);
                break;

            case 'title':
                $card['TITLE'] = $val;
                break;

            case 'company':
                $card['ORG'] = $val;
                break;

            case 'notes':
                $card['NOTE'] = $val;
                break;

            case 'website':
                $card['URL'] = $val;
                break;
            }
        }

        $card['N'] = implode(';', array(
            VCARD_N_FAMILY          => isset($hash['lastname']) ? $hash['lastname'] : '',
            VCARD_N_GIVEN           => isset($hash['firstname']) ? $hash['firstname'] : '',
            VCARD_N_ADDL            => '',
            VCARD_N_PREFIX          => '',
            VCARD_N_SUFFIX          => '',
        ));

        $card['ADR;TYPE=HOME'] = implode(';', array(
            VCARD_ADR_POB           => '',
            VCARD_ADR_EXTEND        => '',
            VCARD_ADR_STREET        => isset($hash['homeAddress']) ? $hash['homeAddress'] : '',
            VCARD_ADR_LOCALITY      => isset($hash['homeCity']) ? $hash['homeCity'] : '',
            VCARD_ADR_REGION        => isset($hash['homeProvince']) ? $hash['homeProvince'] : '',
            VCARD_ADR_POSTCODE      => isset($hash['homePostalCode']) ? $hash['homePostalCode'] : '',
            VCARD_ADR_COUNTRY       => isset($hash['homeCountry']) ? $hash['homeCountry'] : '',
        ));

        $card['ADR;TYPE=WORK'] = implode(';', array(
            VCARD_ADR_POB           => '',
            VCARD_ADR_EXTEND        => '',
            VCARD_ADR_STREET        => isset($hash['workAddress']) ? $hash['workAddress'] : '',
            VCARD_ADR_LOCALITY      => isset($hash['workCity']) ? $hash['workCity'] : '',
            VCARD_ADR_REGION        => isset($hash['workProvince']) ? $hash['workProvince'] : '',
            VCARD_ADR_POSTCODE      => isset($hash['workPostalCode']) ? $hash['workPostalCode'] : '',
            VCARD_ADR_COUNTRY       => isset($hash['workCountry']) ? $hash['workCountry'] : '',
        ));

        return $card;
    }

    /**
     * Takes all necessary actions for the given import step, parameters and
     * form values and returns the next necessary step.
     *
     * @access public
     *
     * @param integer $action        The current step. One of the IMPORT_*
     *                               constants.
     * @param optional array $param  An associative array containing needed
     *                               parameters for the current step.
     *
     * @return mixed        Either the next step as an integer constant or imported
     *                      data set after the final step.
     */
    function nextStep($action, $param = array())
    {
        switch ($action) {
        case IMPORT_FILE:
            $res = parent::nextStep($action, $param);
            if (is_a($res, 'PEAR_Error')) {
                return $res;
            }

            $import_data = $this->importFile($_FILES['import_file']['tmp_name']);
            if (is_a($import_data, 'PEAR_Error')) {
                return $import_data;
            }

            /* Build the result data set as an associative array. */
            $data = array();
            foreach ($import_data as $object) {
                if ($object['type'] == 'VCARD') {
                    $data[] = $this->toHash($object);
                }
            }
            return $data;
            break;

        default:
            return parent::nextStep($action, $param);
            break;
        }
    }

}

--- NEW FILE: vnote.php ---
<?php

/** We rely on the Horde_Data_imc:: abstract class. */
require_once dirname(__FILE__) . '/imc.php';

/**
 * Implement the Horde_Data:: API for vNote data.
 *
 * $Horde: framework/Data/Data/vnote.php,v 1.6 2004/02/24 19:49:03 chuck Exp $
 *
 * Copyright 1999-2004 Jan Schneider <jan at horde.org>
 * Copyright 1999-2004 Chuck Hagenbuch <chuck at horde.org>
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
 *
 * @author  Chuck Hagenbuch <chuck at horde.org>
 * @package Horde_Data
 * @since   Horde 3.0
 */
class Horde_Data_vnote extends Horde_Data_imc {

    /**
     * The vNote version.
     *
     * @access private
     * @var string $_version
     */
    var $_version;

    function importData($text)
    {
        $res = parent::importData($text);
        if (is_a($res, 'PEAR_Error')) {
            return $res;
        }
        return $this->_objects;
    }

    /**
     * Builds a vNote file from a given data structure and returns it
     * as a string.
     *
     * @access public
     *
     * @param array $data  A two-dimensional array containing the data set.
     *
     * @return string  The vNote data.
     */
    function exportData($data)
    {
        /* According to RFC 2425, we should always use CRLF-terminated
         * lines. */
        $newline = "\r\n";

        $file = "BEGIN:VNOTE${newline}VERSION:1.1${newline}";
        foreach ($data as $key => $val) {
            if (!empty($val)) {
                // Basic encoding. Newlines for now; more work here to
                // make this RFC-compliant.
                $file .= $key . ':' .  $this->_quoteAndFold($val);
            }
        }
        $file .= "END:VNOTE$newline";

        return $file;
    }

    /**
     * Builds a vNote file from a given data structure and triggers
     * its download. It DOES NOT exit the current script but only
     * outputs the correct headers and data.
     *
     * @access public
     *
     * @param string $filename   The name of the file to be downloaded.
     * @param array  $data       A two-dimensional array containing the data set.
     */
    function exportFile($filename, $data)
    {
        $export = $this->exportData($data);
        $GLOBALS['browser']->downloadHeaders($filename, 'text/plain', false, strlen($export));
        echo $export;
    }

    /**
     * Return a data hash from a vNote object.
     *
     * @param array $note  The note to convert.
     *
     * @return array  The hashed data.
     *
     * @since Horde 3.0
     */
    function toHash($note)
    {
        $this->_version = isset($note['version']) ? $note['version'] : null;
        $hash = array();
        foreach ($note['params'] as $item) {
            switch ($item['name']) {
            case 'DCREATED':
                $hash['created'] = $this->mapDate($this->read($item));
                break;

            case 'LAST-MODIFIED':
                $hash['modified'] = $this->mapDate($this->read($item));
                break;

            case 'BODY':
                $hash['body'] = $this->readAll($item);
                break;
            }
        }

        return $hash;
    }

    /**
     * Turn a hash (of the same format that we output in
     * Horde_Data_vnote) into an array of vNote data.
     *
     * @param array $hash  The hash of attributes.
     *
     * @return array  The array of vNote attributes -> vNote values.
     *
     * @since Horde 3.0
     */
    function fromHash($hash)
    {
        $note = array();
        foreach ($hash as $key => $val) {
            switch ($key) {
            case 'created':
                $note['DCREATED'] = $this->makeDate((object)$val);
                break;

            case 'modified':
                $note['LAST-MODIFIED'] = $this->makeDate((object)$val);
                break;

            case 'body':
                $note['BODY'] = $val;
                break;
            }
        }

        return $note;
    }

    /**
     * Takes all necessary actions for the given import step,
     * parameters and form values and returns the next necessary step.
     *
     * @access public
     *
     * @param integer $action        The current step. One of the IMPORT_*
     *                               constants.
     * @param optional array $param  An associative array containing needed
     *                               parameters for the current step.
     *
     * @return mixed        Either the next step as an integer constant or imported
     *                      data set after the final step.
     */
    function nextStep($action, $param = array())
    {
        switch ($action) {
        case IMPORT_FILE:
            $res = parent::nextStep($action, $param);
            if (is_a($res, 'PEAR_Error')) {
                return $res;
            }

            $import_data = $this->importFile($_FILES['import_file']['tmp_name']);
            if (is_a($import_data, 'PEAR_Error')) {
                return $import_data;
            }

            /* Build the result data set as an associative array. */
            $data = array();
            foreach ($import_data as $object) {
                if ($object['type'] == 'VNOTE') {
                    $data[] = $this->toHash($object);
                }
            }
            return $data;
            break;

        default:
            return parent::nextStep($action, $param);
            break;
        }
    }

    function _build($data, &$i)
    {
        $objects = array();

        while (isset($data[$i])) {
            if (String::upper($data[$i]['name']) != 'BEGIN') {
                return PEAR::raiseError(sprintf(_("Import Error: Expected \"BEGIN\" on line %d."), $i));
            }
            $type = String::upper($data[$i]['values'][0]);
            $object = array('type' => $type);
            $object['objects'] = array();
            $object['params'] = array();
            $i++;
            while (isset($data[$i]) && String::upper($data[$i]['name']) != 'END') {
                if (String::upper($data[$i]['name']) == 'BEGIN') {
                    $object['objects'][] = $this->_build($data, $i);
                } else {
                    $object['params'][] = $data[$i];
                    if (String::upper($data[$i]['name']) == 'VERSION') {
                        $object['version'] = $data[$i]['values'][0];
                    }
                }
                $i++;
            }
            if (!isset($data[$i])) {
                return PEAR::raiseError(_("Import Error: Unexpected end of file."));
            }
            if (String::upper($data[$i]['values'][0]) != $type) {
                return PEAR::raiseError(sprintf(_("Import Error: Type mismatch. Expected \"END:%s\" on line %d."), $type, $i));
            }
            $objects[] = $object;
            $i++;
        }

        $this->_objects = $objects;
    }

}

--- NEW FILE: vtodo.php ---
<?php

include_once 'Horde/iCalendar.php';

/**
 * Implement the Horde_Data:: API for vTodo data.
 *
 * $Horde: framework/Data/Data/vtodo.php,v 1.6 2004/04/07 14:43:06 chuck Exp $
 *
 * Copyright 1999-2004 Jan Schneider <jan at horde.org>
 * Copyright 1999-2004 Chuck Hagenbuch <chuck at horde.org>
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
 *
 * @author  Chuck Hagenbuch <chuck at horde.org>
 * @package Horde_Data
 * @since   Horde 3.0
 */
class Horde_Data_vtodo extends Horde_Data {

    function importData($text)
    {
        $iCal = &new Horde_iCalendar();
        if (!$iCal->parsevCalendar($text)) {
            return PEAR::raiseError('Error parsing iCalendar text.');
        }

        return $iCal->getComponents();
    }

    /**
     * Builds a vTodo file from a given data structure and returns it
     * as a string.
     *
     * @access public
     *
     * @param array $data  A two-dimensional array containing the data set.
     *
     * @return string  The vTodo data.
     */
    function exportData($data)
    {
        $iCal = &new Horde_iCalendar();

        foreach ($data as $todo) {
            $vTodo = Horde_iCalendar::newComponent('vtodo', $iCal);
            $vTodo->fromArray($todo);

            $iCal->addComponent($vTodo);
        }

        return $iCal->exportvCalendar();
    }

    /**
     * Builds a vTodo file from a given data structure and triggers
     * its download. It DOES NOT exit the current script but only
     * outputs the correct headers and data.
     *
     * @access public
     *
     * @param string $filename   The name of the file to be downloaded.
     * @param array  $data       A two-dimensional array containing the data set.
     */
    function exportFile($filename, $data)
    {
        $export = $this->exportData($data);
        $GLOBALS['browser']->downloadHeaders($filename, 'text/calendar', false, strlen($export));
        echo $export;
    }

    /**
     * Takes all necessary actions for the given import step,
     * parameters and form values and returns the next necessary step.
     *
     * @access public
     *
     * @param integer $action        The current step. One of the IMPORT_*
     *                               constants.
     * @param optional array $param  An associative array containing needed
     *                               parameters for the current step.
     *
     * @return mixed  Either the next step as an integer constant or imported
     *                data set after the final step.
     */
    function nextStep($action, $param = array())
    {
        switch ($action) {
        case IMPORT_FILE:
            $res = parent::nextStep($action, $param);
            if (is_a($res, 'PEAR_Error')) {
                return $res;
            }

            $import_data = $this->importFile($_FILES['import_file']['tmp_name']);
            if (is_a($import_data, 'PEAR_Error')) {
                return $import_data;
            }

            /* Build the result data set as an associative array. */
            $data = array();
            foreach ($import_data as $vtodo) {
                if (is_a($vtodo, 'Horde_iCalendar_vtodo')) {
                    $data[] = $vtodo->toArray();
                }
            }
            return $data;
            break;

        default:
            return parent::nextStep($action, $param);
            break;
        }
    }

}





More information about the commits mailing list