]> git.mjollnir.org Git - m.o.git/commitdiff
initial checkin
authorPenny Leach <penny@hippolyta.local>
Fri, 25 Apr 2008 12:37:44 +0000 (00:37 +1200)
committerPenny Leach <penny@hippolyta.local>
Fri, 25 Apr 2008 12:37:44 +0000 (00:37 +1200)
36 files changed:
.gitignore [new file with mode: 0644]
comp/blog.php [new file with mode: 0644]
comp/flickr.php [new file with mode: 0644]
comp/lastfm.php [new file with mode: 0644]
comp/main.php [new file with mode: 0644]
comp/twitter.php [new file with mode: 0644]
config-dist.php [new file with mode: 0644]
include/main.inc [new file with mode: 0644]
index.php [new file with mode: 0644]
lib/Cache/Lite.php [new file with mode: 0644]
lib/Cache/Lite/File.php [new file with mode: 0644]
lib/Cache/Lite/Function.php [new file with mode: 0644]
lib/Cache/Lite/Output.php [new file with mode: 0644]
lib/HTTP/Request.php [new file with mode: 0644]
lib/HTTP/Request/Listener.php [new file with mode: 0644]
lib/Net/Socket.php [new file with mode: 0644]
lib/Net/URL.php [new file with mode: 0644]
lib/PEAR.php [new file with mode: 0644]
lib/PEAR/Exception.php [new file with mode: 0644]
lib/Snoopy.class.php [new file with mode: 0644]
lib/XML/Feed/Parser.php [new file with mode: 0644]
lib/XML/Feed/Parser/Atom.php [new file with mode: 0644]
lib/XML/Feed/Parser/AtomElement.php [new file with mode: 0644]
lib/XML/Feed/Parser/Exception.php [new file with mode: 0644]
lib/XML/Feed/Parser/RSS09.php [new file with mode: 0644]
lib/XML/Feed/Parser/RSS09Element.php [new file with mode: 0644]
lib/XML/Feed/Parser/RSS1.php [new file with mode: 0644]
lib/XML/Feed/Parser/RSS11.php [new file with mode: 0644]
lib/XML/Feed/Parser/RSS11Element.php [new file with mode: 0644]
lib/XML/Feed/Parser/RSS1Element.php [new file with mode: 0644]
lib/XML/Feed/Parser/RSS2.php [new file with mode: 0644]
lib/XML/Feed/Parser/RSS2Element.php [new file with mode: 0644]
lib/XML/Feed/Parser/Type.php [new file with mode: 0644]
lib/general.php [new file with mode: 0644]
lib/phpFlickr.php [new file with mode: 0644]
style.css [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..b74decb
--- /dev/null
@@ -0,0 +1 @@
+/config.php
diff --git a/comp/blog.php b/comp/blog.php
new file mode 100644 (file)
index 0000000..dd39326
--- /dev/null
@@ -0,0 +1,15 @@
+<?php
+
+function comp_blog() {
+    echo '<h2>latest blog posts</h2>';
+    if ($data = parse_rss(get_config('blog_rss'))) {
+        echo '<ul>';
+        foreach ($data->content as $item) {
+            echo '<li><a href="' . $item->link . '">' . $item->title . '</a></li>';
+        }
+        echo '<li class="last"><a href="http://she.geek.nz/"> ... more</a></li>';
+        echo '</ul>';
+    }
+}
+
+?>
diff --git a/comp/flickr.php b/comp/flickr.php
new file mode 100644 (file)
index 0000000..a0d9595
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+
+
+function comp_flickr() {
+    require_once(get_config('lib') . 'phpFlickr.php');
+    $f = new phpFlickr(get_config('flickr_api_key'));
+    $f->enableCache("fs", get_config('flickr_cache'), get_config('cache_lifetime'));
+
+    // Find the NSID of the username
+
+    $person = $f->people_findByUsername(get_config('flickr_username'));
+    $photos_url = $f->urls_getUserPhotos($person['id']);
+    $photos = $f->people_getPublicPhotos($person['id'], NULL, 9);
+
+    foreach ((array)$photos['photo'] as $photo) {
+        echo "<a href=$photos_url$photo[id]>";
+        echo "<img border='0' alt='$photo[title]' ".
+            "src=" . $f->buildPhotoURL($photo, "Square") . ">";
+        echo "</a>";
+        $i++;
+    }
+}
diff --git a/comp/lastfm.php b/comp/lastfm.php
new file mode 100644 (file)
index 0000000..5435e20
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+
+function comp_lastfm() {
+    echo '<h2>recently listened tracks</h2>';
+    if ($data = parse_rss('http://ws.audioscrobbler.com/1.0/user/' . get_config('lastfm_username') . '/recenttracks.rss')) {
+        echo '<ul>';
+        foreach ($data->content as $item) {
+            echo '<li><a href="' . $item->link . '">' . $item->title  . '</a></li>';
+        }
+        echo '<li class="last"><a href="http://last.fm/user/mjollnir_/">... more</a></li>';
+        echo '</ul>';
+    }
+
+}
diff --git a/comp/main.php b/comp/main.php
new file mode 100644 (file)
index 0000000..61e1946
--- /dev/null
@@ -0,0 +1,5 @@
+<?php
+
+function comp_main() {
+    include(get_config('root') . '/include/main.inc');
+}
diff --git a/comp/twitter.php b/comp/twitter.php
new file mode 100644 (file)
index 0000000..1065b9c
--- /dev/null
@@ -0,0 +1,15 @@
+<?php
+
+function comp_twitter() {
+    echo '<h2>latest twitter updates</h2>';
+    if ($data = parse_rss('http://twitter.com/statuses/user_timeline/' . get_config('twitter_username') . '.rss')) {
+        echo '<ul>';
+        foreach ($data->content as $item) {
+            echo '<li><a href="' . $item->link . '">' . substr($item->title, strlen(get_config('twitter_username')) + 2) . '</a></li>';
+        }
+        echo '<li class="last"><a href="http://twitter.com/mjollnir/">... more</a></li>';
+        echo '</ul>';
+    }
+}
+
+?>
diff --git a/config-dist.php b/config-dist.php
new file mode 100644 (file)
index 0000000..9bd5005
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+$CFG = array(
+    'flickr_api_key'    => '',
+    'flickr_api_secret' => '',
+    'flickr_username'   => '',
+    'flickr_cache'      => '',
+    'twitter_username'  => '',
+    'lastfm_username'   => '',
+    'blog_rss'          => '',
+    'rss_cache'         => '',
+    'curl'              => '',
+    'cache_lifetime'    => 0,
+);
+
+// autodetect stuff
+$CFG['lib'] = dirname(__FILE__) . '/lib/';
+$CFG['root'] = dirname(__FILE__) . '/';
+$CFG['comp'] = dirname(__FILE__) . '/comp/';
+
+?>
diff --git a/include/main.inc b/include/main.inc
new file mode 100644 (file)
index 0000000..6f23673
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+    $whorepile = array(
+        'twitter'    => 'http://twitter.com/mjollnir',
+        'facebook'   => 'http://www.facebook.com/profile.php?id=571239084',
+        'last.fm'    => 'http://www.last.fm/user/mjollnir_/',
+        'flickr'     => 'http://www.flickr.com/photos/penzilinha/',
+        'friendfeed' => 'http://friendfeed.com/mjollnir',
+        'pownce'     => 'http://pownce.com/mjollnir/',
+        'ohloh'      => 'http://www.ohloh.net/accounts/3438',
+);
+
+?>
+<h2>mjollnir.org</h2>
+<p>a collection of what I'm doing all over the internet, mashed together into one place for your viewing pleasure.</p>
+<p>my blog lives at <a href="http://she.geek.nz">she.geek.nz</a>. there's an <a href="http://she.geek.nz/about/">about page</a> there too. it's old and rather out of date.</p>
+<p>find me on <?php
+    $count = 0;
+    foreach ($whorepile as $service => $url) {
+        echo '<a href="' . $url . '">' . $service . '</a>';
+        if ($count != count($whorepile) -1) {
+            echo ', ';
+        }
+        $count++;
+    }
+?>.  christ.  at least I never got livejournal.</p>
+<p>I don't have an rss feed for this mashup. use friendfeed instead, or an individual site's feed.</p>
+<p>thanks to <a href="http://nothing.net.nz">vex</a> for the vserver</p>
+<p><a href="mailto:penny@mjollnir.org">send me email!</a>
+<span class="mind"><b>On my mind</b>: <a href="http://mahara.org">mahara</a>, more tattoos, less tattoos, moodle gsoc, Europe</span>
diff --git a/index.php b/index.php
new file mode 100644 (file)
index 0000000..97e7f50
--- /dev/null
+++ b/index.php
@@ -0,0 +1,26 @@
+<?php
+    require_once(dirname(__FILE__) . '/config.php');
+    require_once(dirname(__FILE__) . '/lib/general.php');
+    $components = get_components();
+echo '<?xml version="1.0" encoding="UTF-8"?>';
+?>
+<!DOCTYPE html
+     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+    <head>
+        <title>mjollnir.org</title>
+        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+        <link rel="stylesheet" type="text/css" href="style.css" />
+    </head>
+    <body>
+<?php
+    foreach ($components as $comp) {
+        echo '        <div class="component" id="' . $comp . '">';
+        require_once(get_config('comp') . $comp . '.php');
+        call_user_func('comp_' . $comp);
+        echo '        </div>';
+    }
+?>
+    </body>
+</html>
diff --git a/lib/Cache/Lite.php b/lib/Cache/Lite.php
new file mode 100644 (file)
index 0000000..335b15d
--- /dev/null
@@ -0,0 +1,826 @@
+<?php
+
+/**
+* Fast, light and safe Cache Class
+*
+* Cache_Lite is a fast, light and safe cache system. It's optimized
+* for file containers. It is fast and safe (because it uses file
+* locking and/or anti-corruption tests).
+*
+* There are some examples in the 'docs/examples' file
+* Technical choices are described in the 'docs/technical' file
+*
+* Memory Caching is from an original idea of
+* Mike BENOIT <ipso@snappymail.ca>
+*
+* Nota : A chinese documentation (thanks to RainX <china_1982@163.com>) is
+* available at :
+* http://rainx.phpmore.com/manual/cache_lite.html
+*
+* @package Cache_Lite
+* @category Caching
+* @version $Id: Lite.php,v 1.50 2008/04/13 14:41:23 tacker Exp $
+* @author Fabien MARTY <fab@php.net>
+*/
+
+define('CACHE_LITE_ERROR_RETURN', 1);
+define('CACHE_LITE_ERROR_DIE', 8);
+
+class Cache_Lite
+{
+
+    // --- Private properties ---
+
+    /**
+    * Directory where to put the cache files
+    * (make sure to add a trailing slash)
+    *
+    * @var string $_cacheDir
+    */
+    var $_cacheDir = '/tmp/';
+
+    /**
+    * Enable / disable caching
+    *
+    * (can be very usefull for the debug of cached scripts)
+    *
+    * @var boolean $_caching
+    */
+    var $_caching = true;
+
+    /**
+    * Cache lifetime (in seconds)
+    *
+    * If null, the cache is valid forever.
+    *
+    * @var int $_lifeTime
+    */
+    var $_lifeTime = 3600;
+
+    /**
+    * Enable / disable fileLocking
+    *
+    * (can avoid cache corruption under bad circumstances)
+    *
+    * @var boolean $_fileLocking
+    */
+    var $_fileLocking = true;
+
+    /**
+    * Timestamp of the last valid cache
+    *
+    * @var int $_refreshTime
+    */
+    var $_refreshTime;
+
+    /**
+    * File name (with path)
+    *
+    * @var string $_file
+    */
+    var $_file;
+    
+    /**
+    * File name (without path)
+    *
+    * @var string $_fileName
+    */
+    var $_fileName;
+
+    /**
+    * Enable / disable write control (the cache is read just after writing to detect corrupt entries)
+    *
+    * Enable write control will lightly slow the cache writing but not the cache reading
+    * Write control can detect some corrupt cache files but maybe it's not a perfect control
+    *
+    * @var boolean $_writeControl
+    */
+    var $_writeControl = true;
+
+    /**
+    * Enable / disable read control
+    *
+    * If enabled, a control key is embeded in cache file and this key is compared with the one
+    * calculated after the reading.
+    *
+    * @var boolean $_writeControl
+    */
+    var $_readControl = true;
+
+    /**
+    * Type of read control (only if read control is enabled)
+    *
+    * Available values are :
+    * 'md5' for a md5 hash control (best but slowest)
+    * 'crc32' for a crc32 hash control (lightly less safe but faster, better choice)
+    * 'strlen' for a length only test (fastest)
+    *
+    * @var boolean $_readControlType
+    */
+    var $_readControlType = 'crc32';
+
+    /**
+    * Pear error mode (when raiseError is called)
+    *
+    * (see PEAR doc)
+    *
+    * @see setToDebug()
+    * @var int $_pearErrorMode
+    */
+    var $_pearErrorMode = CACHE_LITE_ERROR_RETURN;
+    
+    /**
+    * Current cache id
+    *
+    * @var string $_id
+    */
+    var $_id;
+
+    /**
+    * Current cache group
+    *
+    * @var string $_group
+    */
+    var $_group;
+
+    /**
+    * Enable / Disable "Memory Caching"
+    *
+    * NB : There is no lifetime for memory caching ! 
+    *
+    * @var boolean $_memoryCaching
+    */
+    var $_memoryCaching = false;
+
+    /**
+    * Enable / Disable "Only Memory Caching"
+    * (be carefull, memory caching is "beta quality")
+    *
+    * @var boolean $_onlyMemoryCaching
+    */
+    var $_onlyMemoryCaching = false;
+
+    /**
+    * Memory caching array
+    *
+    * @var array $_memoryCachingArray
+    */
+    var $_memoryCachingArray = array();
+
+    /**
+    * Memory caching counter
+    *
+    * @var int $memoryCachingCounter
+    */
+    var $_memoryCachingCounter = 0;
+
+    /**
+    * Memory caching limit
+    *
+    * @var int $memoryCachingLimit
+    */
+    var $_memoryCachingLimit = 1000;
+    
+    /**
+    * File Name protection
+    *
+    * if set to true, you can use any cache id or group name
+    * if set to false, it can be faster but cache ids and group names
+    * will be used directly in cache file names so be carefull with
+    * special characters...
+    *
+    * @var boolean $fileNameProtection
+    */
+    var $_fileNameProtection = true;
+    
+    /**
+    * Enable / disable automatic serialization
+    *
+    * it can be used to save directly datas which aren't strings
+    * (but it's slower)    
+    *
+    * @var boolean $_serialize
+    */
+    var $_automaticSerialization = false;
+    
+    /**
+    * Disable / Tune the automatic cleaning process
+    *
+    * The automatic cleaning process destroy too old (for the given life time)
+    * cache files when a new cache file is written.
+    * 0               => no automatic cache cleaning
+    * 1               => systematic cache cleaning
+    * x (integer) > 1 => automatic cleaning randomly 1 times on x cache write
+    *
+    * @var int $_automaticCleaning
+    */
+    var $_automaticCleaningFactor = 0;
+    
+    /**
+    * Nested directory level
+    *
+    * Set the hashed directory structure level. 0 means "no hashed directory 
+    * structure", 1 means "one level of directory", 2 means "two levels"... 
+    * This option can speed up Cache_Lite only when you have many thousands of 
+    * cache file. Only specific benchs can help you to choose the perfect value 
+    * for you. Maybe, 1 or 2 is a good start.
+    *
+    * @var int $_hashedDirectoryLevel
+    */
+    var $_hashedDirectoryLevel = 0;
+    
+    /**
+    * Umask for hashed directory structure
+    *
+    * @var int $_hashedDirectoryUmask
+    */
+    var $_hashedDirectoryUmask = 0700;
+    
+    /**
+     * API break for error handling in CACHE_LITE_ERROR_RETURN mode
+     * 
+     * In CACHE_LITE_ERROR_RETURN mode, error handling was not good because
+     * for example save() method always returned a boolean (a PEAR_Error object
+     * would be better in CACHE_LITE_ERROR_RETURN mode). To correct this without
+     * breaking the API, this option (false by default) can change this handling.
+     * 
+     * @var boolean
+     */
+    var $_errorHandlingAPIBreak = false;
+    
+    // --- Public methods ---
+
+    /**
+    * Constructor
+    *
+    * $options is an assoc. Available options are :
+    * $options = array(
+    *     'cacheDir' => directory where to put the cache files (string),
+    *     'caching' => enable / disable caching (boolean),
+    *     'lifeTime' => cache lifetime in seconds (int),
+    *     'fileLocking' => enable / disable fileLocking (boolean),
+    *     'writeControl' => enable / disable write control (boolean),
+    *     'readControl' => enable / disable read control (boolean),
+    *     'readControlType' => type of read control 'crc32', 'md5', 'strlen' (string),
+    *     'pearErrorMode' => pear error mode (when raiseError is called) (cf PEAR doc) (int),
+    *     'memoryCaching' => enable / disable memory caching (boolean),
+    *     'onlyMemoryCaching' => enable / disable only memory caching (boolean),
+    *     'memoryCachingLimit' => max nbr of records to store into memory caching (int),
+    *     'fileNameProtection' => enable / disable automatic file name protection (boolean),
+    *     'automaticSerialization' => enable / disable automatic serialization (boolean),
+    *     'automaticCleaningFactor' => distable / tune automatic cleaning process (int),
+    *     'hashedDirectoryLevel' => level of the hashed directory system (int),
+    *     'hashedDirectoryUmask' => umask for hashed directory structure (int),
+    *     'errorHandlingAPIBreak' => API break for better error handling ? (boolean)
+    * );
+    *
+    * @param array $options options
+    * @access public
+    */
+    function Cache_Lite($options = array(NULL))
+    {
+        foreach($options as $key => $value) {
+            $this->setOption($key, $value);
+        }
+    }
+    
+    /**
+    * Generic way to set a Cache_Lite option
+    *
+    * see Cache_Lite constructor for available options
+    *
+    * @var string $name name of the option
+    * @var mixed $value value of the option
+    * @access public
+    */
+    function setOption($name, $value) 
+    {
+        $availableOptions = array('errorHandlingAPIBreak', 'hashedDirectoryUmask', 'hashedDirectoryLevel', 'automaticCleaningFactor', 'automaticSerialization', 'fileNameProtection', 'memoryCaching', 'onlyMemoryCaching', 'memoryCachingLimit', 'cacheDir', 'caching', 'lifeTime', 'fileLocking', 'writeControl', 'readControl', 'readControlType', 'pearErrorMode');
+        if (in_array($name, $availableOptions)) {
+            $property = '_'.$name;
+            $this->$property = $value;
+        }
+    }
+    
+    /**
+    * Test if a cache is available and (if yes) return it
+    *
+    * @param string $id cache id
+    * @param string $group name of the cache group
+    * @param boolean $doNotTestCacheValidity if set to true, the cache validity won't be tested
+    * @return string data of the cache (else : false)
+    * @access public
+    */
+    function get($id, $group = 'default', $doNotTestCacheValidity = false)
+    {
+        $this->_id = $id;
+        $this->_group = $group;
+        $data = false;
+        if ($this->_caching) {
+            $this->_setRefreshTime();
+            $this->_setFileName($id, $group);
+            clearstatcache();
+            if ($this->_memoryCaching) {
+                if (isset($this->_memoryCachingArray[$this->_file])) {
+                    if ($this->_automaticSerialization) {
+                        return unserialize($this->_memoryCachingArray[$this->_file]);
+                    }
+                    return $this->_memoryCachingArray[$this->_file];
+                }
+                if ($this->_onlyMemoryCaching) {
+                    return false;
+                }                
+            }
+            if (($doNotTestCacheValidity) || (is_null($this->_refreshTime))) {
+                if (file_exists($this->_file)) {
+                    $data = $this->_read();
+                }
+            } else {
+                if ((file_exists($this->_file)) && (@filemtime($this->_file) > $this->_refreshTime)) {
+                    $data = $this->_read();
+                }
+            }
+            if (($data) and ($this->_memoryCaching)) {
+                $this->_memoryCacheAdd($data);
+            }
+            if (($this->_automaticSerialization) and (is_string($data))) {
+                $data = unserialize($data);
+            }
+            return $data;
+        }
+        return false;
+    }
+    
+    /**
+    * Save some data in a cache file
+    *
+    * @param string $data data to put in cache (can be another type than strings if automaticSerialization is on)
+    * @param string $id cache id
+    * @param string $group name of the cache group
+    * @return boolean true if no problem (else : false or a PEAR_Error object)
+    * @access public
+    */
+    function save($data, $id = NULL, $group = 'default')
+    {
+        if ($this->_caching) {
+            if ($this->_automaticSerialization) {
+                $data = serialize($data);
+            }
+            if (isset($id)) {
+                $this->_setFileName($id, $group);
+            }
+            if ($this->_memoryCaching) {
+                $this->_memoryCacheAdd($data);
+                if ($this->_onlyMemoryCaching) {
+                    return true;
+                }
+            }
+            if ($this->_automaticCleaningFactor>0) {
+                $rand = rand(1, $this->_automaticCleaningFactor);
+                if ($rand==1) {
+                    $this->clean(false, 'old');
+                }
+            }
+            if ($this->_writeControl) {
+                $res = $this->_writeAndControl($data);
+                if (is_bool($res)) {
+                    if ($res) {
+                        return true;  
+                    }
+                    // if $res if false, we need to invalidate the cache
+                    @touch($this->_file, time() - 2*abs($this->_lifeTime));
+                    return false;
+                }            
+            } else {
+                $res = $this->_write($data);
+            }
+            if (is_object($res)) {
+                // $res is a PEAR_Error object 
+                if (!($this->_errorHandlingAPIBreak)) {   
+                    return false; // we return false (old API)
+                }
+            }
+            return $res;
+        }
+        return false;
+    }
+
+    /**
+    * Remove a cache file
+    *
+    * @param string $id cache id
+    * @param string $group name of the cache group
+    * @return boolean true if no problem
+    * @access public
+    */
+    function remove($id, $group = 'default')
+    {
+        $this->_setFileName($id, $group);
+        if ($this->_memoryCaching) {
+            if (isset($this->_memoryCachingArray[$this->_file])) {
+                unset($this->_memoryCachingArray[$this->_file]);
+                $this->_memoryCachingCounter = $this->_memoryCachingCounter - 1;
+            }
+            if ($this->_onlyMemoryCaching) {
+                return true;
+            }
+        }
+        return $this->_unlink($this->_file);
+    }
+
+    /**
+    * Clean the cache
+    *
+    * if no group is specified all cache files will be destroyed
+    * else only cache files of the specified group will be destroyed
+    *
+    * @param string $group name of the cache group
+    * @param string $mode flush cache mode : 'old', 'ingroup', 'notingroup', 
+    *                                        'callback_myFunction'
+    * @return boolean true if no problem
+    * @access public
+    */
+    function clean($group = false, $mode = 'ingroup')
+    {
+        return $this->_cleanDir($this->_cacheDir, $group, $mode);
+    }
+       
+    /**
+    * Set to debug mode
+    *
+    * When an error is found, the script will stop and the message will be displayed
+    * (in debug mode only). 
+    *
+    * @access public
+    */
+    function setToDebug()
+    {
+        $this->setOption('pearErrorMode', CACHE_LITE_ERROR_DIE);
+    }
+
+    /**
+    * Set a new life time
+    *
+    * @param int $newLifeTime new life time (in seconds)
+    * @access public
+    */
+    function setLifeTime($newLifeTime)
+    {
+        $this->_lifeTime = $newLifeTime;
+        $this->_setRefreshTime();
+    }
+
+    /**
+    * Save the state of the caching memory array into a cache file cache
+    *
+    * @param string $id cache id
+    * @param string $group name of the cache group
+    * @access public
+    */
+    function saveMemoryCachingState($id, $group = 'default')
+    {
+        if ($this->_caching) {
+            $array = array(
+                'counter' => $this->_memoryCachingCounter,
+                'array' => $this->_memoryCachingArray
+            );
+            $data = serialize($array);
+            $this->save($data, $id, $group);
+        }
+    }
+
+    /**
+    * Load the state of the caching memory array from a given cache file cache
+    *
+    * @param string $id cache id
+    * @param string $group name of the cache group
+    * @param boolean $doNotTestCacheValidity if set to true, the cache validity won't be tested
+    * @access public
+    */
+    function getMemoryCachingState($id, $group = 'default', $doNotTestCacheValidity = false)
+    {
+        if ($this->_caching) {
+            if ($data = $this->get($id, $group, $doNotTestCacheValidity)) {
+                $array = unserialize($data);
+                $this->_memoryCachingCounter = $array['counter'];
+                $this->_memoryCachingArray = $array['array'];
+            }
+        }
+    }
+    
+    /**
+    * Return the cache last modification time
+    *
+    * BE CAREFUL : THIS METHOD IS FOR HACKING ONLY !
+    *
+    * @return int last modification time
+    */
+    function lastModified() 
+    {
+        return @filemtime($this->_file);
+    }
+    
+    /**
+    * Trigger a PEAR error
+    *
+    * To improve performances, the PEAR.php file is included dynamically.
+    * The file is so included only when an error is triggered. So, in most
+    * cases, the file isn't included and perfs are much better.
+    *
+    * @param string $msg error message
+    * @param int $code error code
+    * @access public
+    */
+    function raiseError($msg, $code)
+    {
+        include_once('PEAR.php');
+        return PEAR::raiseError($msg, $code, $this->_pearErrorMode);
+    }
+    
+    /**
+     * Extend the life of a valid cache file
+     * 
+     * see http://pear.php.net/bugs/bug.php?id=6681
+     * 
+     * @access public
+     */
+    function extendLife()
+    {
+        @touch($this->_file);
+    }
+    
+    // --- Private methods ---
+    
+    /**
+    * Compute & set the refresh time
+    *
+    * @access private
+    */
+    function _setRefreshTime() 
+    {
+        if (is_null($this->_lifeTime)) {
+            $this->_refreshTime = null;
+        } else {
+            $this->_refreshTime = time() - $this->_lifeTime;
+        }
+    }
+    
+    /**
+    * Remove a file
+    * 
+    * @param string $file complete file path and name
+    * @return boolean true if no problem
+    * @access private
+    */
+    function _unlink($file)
+    {
+        if (!@unlink($file)) {
+            return $this->raiseError('Cache_Lite : Unable to remove cache !', -3);
+        }
+        return true;        
+    }
+
+    /**
+    * Recursive function for cleaning cache file in the given directory
+    *
+    * @param string $dir directory complete path (with a trailing slash)
+    * @param string $group name of the cache group
+    * @param string $mode flush cache mode : 'old', 'ingroup', 'notingroup',
+                                             'callback_myFunction'
+    * @return boolean true if no problem
+    * @access private
+    */
+    function _cleanDir($dir, $group = false, $mode = 'ingroup')     
+    {
+        if ($this->_fileNameProtection) {
+            $motif = ($group) ? 'cache_'.md5($group).'_' : 'cache_';
+        } else {
+            $motif = ($group) ? 'cache_'.$group.'_' : 'cache_';
+        }
+        if ($this->_memoryCaching) {
+           foreach($this->_memoryCachingArray as $key => $v) {
+                if (strpos($key, $motif) !== false) {\r
+                    unset($this->_memoryCachingArray[$key]);
+                    $this->_memoryCachingCounter = $this->_memoryCachingCounter - 1;
+                }
+            }
+            if ($this->_onlyMemoryCaching) {
+                return true;
+            }
+        }
+        if (!($dh = opendir($dir))) {
+            return $this->raiseError('Cache_Lite : Unable to open cache directory !', -4);
+        }
+        $result = true;
+        while ($file = readdir($dh)) {
+            if (($file != '.') && ($file != '..')) {
+                if (substr($file, 0, 6)=='cache_') {
+                    $file2 = $dir . $file;
+                    if (is_file($file2)) {
+                        switch (substr($mode, 0, 9)) {
+                            case 'old':
+                                // files older than lifeTime get deleted from cache
+                                if (!is_null($this->_lifeTime)) {
+                                    if ((mktime() - @filemtime($file2)) > $this->_lifeTime) {
+                                        $result = ($result and ($this->_unlink($file2)));
+                                    }
+                                }
+                                break;
+                            case 'notingrou':
+                                if (strpos($file2, $motif) === false) {\r
+                                    $result = ($result and ($this->_unlink($file2)));
+                                }
+                                break;
+                            case 'callback_':
+                                $func = substr($mode, 9, strlen($mode) - 9);
+                                if ($func($file2, $group)) {
+                                    $result = ($result and ($this->_unlink($file2)));
+                                }
+                                break;
+                            case 'ingroup':
+                            default:
+                                if (strpos($file2, $motif) !== false) {\r
+                                    $result = ($result and ($this->_unlink($file2)));
+                                }
+                                break;
+                        }
+                    }
+                    if ((is_dir($file2)) and ($this->_hashedDirectoryLevel>0)) {
+                        $result = ($result and ($this->_cleanDir($file2 . '/', $group, $mode)));
+                    }
+                }
+            }
+        }
+        return $result;
+    }
+      
+    /**
+    * Add some date in the memory caching array
+    *
+    * @param string $data data to cache
+    * @access private
+    */
+    function _memoryCacheAdd($data)
+    {
+        $this->_memoryCachingArray[$this->_file] = $data;
+        if ($this->_memoryCachingCounter >= $this->_memoryCachingLimit) {
+            list($key, ) = each($this->_memoryCachingArray);
+            unset($this->_memoryCachingArray[$key]);
+        } else {
+            $this->_memoryCachingCounter = $this->_memoryCachingCounter + 1;
+        }
+    }
+
+    /**
+    * Make a file name (with path)
+    *
+    * @param string $id cache id
+    * @param string $group name of the group
+    * @access private
+    */
+    function _setFileName($id, $group)
+    {
+        
+        if ($this->_fileNameProtection) {
+            $suffix = 'cache_'.md5($group).'_'.md5($id);
+        } else {
+            $suffix = 'cache_'.$group.'_'.$id;
+        }
+        $root = $this->_cacheDir;
+        if ($this->_hashedDirectoryLevel>0) {
+            $hash = md5($suffix);
+            for ($i=0 ; $i<$this->_hashedDirectoryLevel ; $i++) {
+                $root = $root . 'cache_' . substr($hash, 0, $i + 1) . '/';
+            }   
+        }
+        $this->_fileName = $suffix;
+        $this->_file = $root.$suffix;
+    }
+    
+    /**
+    * Read the cache file and return the content
+    *
+    * @return string content of the cache file (else : false or a PEAR_Error object)
+    * @access private
+    */
+    function _read()
+    {
+        $fp = @fopen($this->_file, "rb");
+        if ($this->_fileLocking) @flock($fp, LOCK_SH);
+        if ($fp) {
+            clearstatcache();
+            $length = @filesize($this->_file);
+            $mqr = get_magic_quotes_runtime();
+            set_magic_quotes_runtime(0);
+            if ($this->_readControl) {
+                $hashControl = @fread($fp, 32);
+                $length = $length - 32;
+            } 
+            if ($length) {
+                $data = @fread($fp, $length);
+            } else {
+                $data = '';
+            }
+            set_magic_quotes_runtime($mqr);
+            if ($this->_fileLocking) @flock($fp, LOCK_UN);
+            @fclose($fp);
+            if ($this->_readControl) {
+                $hashData = $this->_hash($data, $this->_readControlType);
+                if ($hashData != $hashControl) {
+                    if (!(is_null($this->_lifeTime))) {
+                        @touch($this->_file, time() - 2*abs($this->_lifeTime)); 
+                    } else {
+                        @unlink($this->_file);
+                    }
+                    return false;
+                }
+            }
+            return $data;
+        }
+        return $this->raiseError('Cache_Lite : Unable to read cache !', -2); 
+    }
+    
+    /**
+    * Write the given data in the cache file
+    *
+    * @param string $data data to put in cache
+    * @return boolean true if ok (a PEAR_Error object else)
+    * @access private
+    */
+    function _write($data)
+    {
+        if ($this->_hashedDirectoryLevel > 0) {
+            $hash = md5($this->_fileName);
+            $root = $this->_cacheDir;
+            for ($i=0 ; $i<$this->_hashedDirectoryLevel ; $i++) {
+                $root = $root . 'cache_' . substr($hash, 0, $i + 1) . '/';
+                if (!(@is_dir($root))) {
+                    @mkdir($root, $this->_hashedDirectoryUmask);
+                }
+            }
+        }
+        $fp = @fopen($this->_file, "wb");
+        if ($fp) {
+            if ($this->_fileLocking) @flock($fp, LOCK_EX);
+            if ($this->_readControl) {
+                @fwrite($fp, $this->_hash($data, $this->_readControlType), 32);
+            }
+            $mqr = get_magic_quotes_runtime();
+            set_magic_quotes_runtime(0);
+            @fwrite($fp, $data);
+            set_magic_quotes_runtime($mqr);
+            if ($this->_fileLocking) @flock($fp, LOCK_UN);
+            @fclose($fp);
+            return true;
+        }      
+        return $this->raiseError('Cache_Lite : Unable to write cache file : '.$this->_file, -1);
+    }
+       
+    /**
+    * Write the given data in the cache file and control it just after to avoir corrupted cache entries
+    *
+    * @param string $data data to put in cache
+    * @return boolean true if the test is ok (else : false or a PEAR_Error object)
+    * @access private
+    */
+    function _writeAndControl($data)
+    {
+        $result = $this->_write($data);
+        if (is_object($result)) {
+            return $result; # We return the PEAR_Error object
+        }
+        $dataRead = $this->_read();
+        if (is_object($dataRead)) {
+            return $dataRead; # We return the PEAR_Error object
+        }
+        if ((is_bool($dataRead)) && (!$dataRead)) {
+            return false; 
+        }
+        return ($dataRead==$data);
+    }
+    
+    /**
+    * Make a control key with the string containing datas
+    *
+    * @param string $data data
+    * @param string $controlType type of control 'md5', 'crc32' or 'strlen'
+    * @return string control key
+    * @access private
+    */
+    function _hash($data, $controlType)
+    {
+        switch ($controlType) {
+        case 'md5':
+            return md5($data);
+        case 'crc32':
+            return sprintf('% 32d', crc32($data));
+        case 'strlen':
+            return sprintf('% 32d', strlen($data));
+        default:
+            return $this->raiseError('Unknown controlType ! (available values are only \'md5\', \'crc32\', \'strlen\')', -5);
+        }
+    }
+    
+} 
+
+?>
diff --git a/lib/Cache/Lite/File.php b/lib/Cache/Lite/File.php
new file mode 100644 (file)
index 0000000..f048acf
--- /dev/null
@@ -0,0 +1,92 @@
+<?php
+
+/**
+* This class extends Cache_Lite and offers a cache system driven by a master file
+*
+* With this class, cache validity is only dependent of a given file. Cache files
+* are valid only if they are older than the master file. It's a perfect way for
+* caching templates results (if the template file is newer than the cache, cache
+* must be rebuild...) or for config classes...
+* There are some examples in the 'docs/examples' file
+* Technical choices are described in the 'docs/technical' file
+*
+* @package Cache_Lite
+* @version $Id: File.php,v 1.3 2005/12/04 16:03:55 fab Exp $
+* @author Fabien MARTY <fab@php.net>
+*/
+require_once('Cache/Lite.php');
+
+class Cache_Lite_File extends Cache_Lite
+{
+
+    // --- Private properties ---
+    
+    /**
+    * Complete path of the file used for controlling the cache lifetime
+    *
+    * @var string $_masterFile
+    */
+    var $_masterFile = '';
+    
+    /**
+    * Masterfile mtime
+    *
+    * @var int $_masterFile_mtime
+    */
+    var $_masterFile_mtime = 0;
+    
+    // --- Public methods ----
+    
+    /**
+    * Constructor
+    *
+    * $options is an assoc. To have a look at availables options,
+    * see the constructor of the Cache_Lite class in 'Cache_Lite.php'
+    *
+    * Comparing to Cache_Lite constructor, there is another option :
+    * $options = array(
+    *     (...) see Cache_Lite constructor
+    *     'masterFile' => complete path of the file used for controlling the cache lifetime(string)
+    * );
+    *
+    * @param array $options options
+    * @access public
+    */
+    function Cache_Lite_File($options = array(NULL))
+    {   
+        $options['lifetime'] = 0;
+        $this->Cache_Lite($options);
+        if (isset($options['masterFile'])) {
+            $this->_masterFile = $options['masterFile'];
+        } else {
+            return $this->raiseError('Cache_Lite_File : masterFile option must be set !');
+        }
+        if (!($this->_masterFile_mtime = @filemtime($this->_masterFile))) {
+            return $this->raiseError('Cache_Lite_File : Unable to read masterFile : '.$this->_masterFile, -3);
+        }
+    }
+    
+    /**
+    * Test if a cache is available and (if yes) return it
+    *
+    * @param string $id cache id
+    * @param string $group name of the cache group
+    * @return string data of the cache (or false if no cache available)
+    * @access public
+    */
+    function get($id, $group = 'default') 
+    {
+        if ($data = parent::get($id, $group, true)) {
+            if ($filemtime = $this->lastModified()) {
+                if ($filemtime > $this->_masterFile_mtime) {
+                    return $data;
+                }
+            }
+        }
+        return false;
+    }
+
+}
+
+?>
diff --git a/lib/Cache/Lite/Function.php b/lib/Cache/Lite/Function.php
new file mode 100644 (file)
index 0000000..63a96d9
--- /dev/null
@@ -0,0 +1,211 @@
+<?php
+
+/**
+* This class extends Cache_Lite and can be used to cache the result and output of functions/methods
+*
+* This class is completly inspired from Sebastian Bergmann's
+* PEAR/Cache_Function class. This is only an adaptation to
+* Cache_Lite
+*
+* There are some examples in the 'docs/examples' file
+* Technical choices are described in the 'docs/technical' file
+*
+* @package Cache_Lite
+* @version $Id: Function.php,v 1.11 2006/12/14 12:59:43 cweiske Exp $
+* @author Sebastian BERGMANN <sb@sebastian-bergmann.de>
+* @author Fabien MARTY <fab@php.net>
+*/
+
+require_once('Cache/Lite.php');
+
+class Cache_Lite_Function extends Cache_Lite
+{
+
+    // --- Private properties ---
+
+    /**
+     * Default cache group for function caching
+     *
+     * @var string $_defaultGroup
+     */
+    var $_defaultGroup = 'Cache_Lite_Function';
+
+    /**
+     * Don't cache the method call when its output contains the string "NOCACHE"
+     *
+     * if set to true, the output of the method will never be displayed (because the output is used
+     * to control the cache)
+     *
+     * @var boolean $_dontCacheWhenTheOutputContainsNOCACHE
+     */
+    var $_dontCacheWhenTheOutputContainsNOCACHE = false;
+
+    /**
+     * Don't cache the method call when its result is false
+     *
+     * @var boolean $_dontCacheWhenTheResultIsFalse
+     */
+    var $_dontCacheWhenTheResultIsFalse = false;
+
+    /**
+     * Don't cache the method call when its result is null
+     *
+     * @var boolean $_dontCacheWhenTheResultIsNull
+     */
+    var $_dontCacheWhenTheResultIsNull = false;
+
+    /**
+     * Debug the Cache_Lite_Function caching process
+     *
+     * @var boolean $_debugCacheLiteFunction
+     */
+    var $_debugCacheLiteFunction = false;
+
+    // --- Public methods ----
+
+    /**
+    * Constructor
+    *
+    * $options is an assoc. To have a look at availables options,
+    * see the constructor of the Cache_Lite class in 'Cache_Lite.php'
+    *
+    * Comparing to Cache_Lite constructor, there is another option :
+    * $options = array(
+    *     (...) see Cache_Lite constructor
+    *     'debugCacheLiteFunction' => (bool) debug the caching process,
+    *     'defaultGroup' => default cache group for function caching (string),
+    *     'dontCacheWhenTheOutputContainsNOCACHE' => (bool) don't cache when the function output contains "NOCACHE",
+    *     'dontCacheWhenTheResultIsFalse' => (bool) don't cache when the function result is false,
+    *     'dontCacheWhenTheResultIsNull' => (bool don't cache when the function result is null
+    * );
+    *
+    * @param array $options options
+    * @access public
+    */
+    function Cache_Lite_Function($options = array(NULL))
+    {
+        $availableOptions = array('debugCacheLiteFunction', 'defaultGroup', 'dontCacheWhenTheOutputContainsNOCACHE', 'dontCacheWhenTheResultIsFalse', 'dontCacheWhenTheResultIsNull');
+        while (list($name, $value) = each($options)) {
+            if (in_array($name, $availableOptions)) {
+                $property = '_'.$name;
+                $this->$property = $value;
+            }
+        }
+        reset($options);
+        $this->Cache_Lite($options);
+    }
+
+    /**
+    * Calls a cacheable function or method (or not if there is already a cache for it)
+    *
+    * Arguments of this method are read with func_get_args. So it doesn't appear
+    * in the function definition. Synopsis :
+    * call('functionName', $arg1, $arg2, ...)
+    * (arg1, arg2... are arguments of 'functionName')
+    *
+    * @return mixed result of the function/method
+    * @access public
+    */
+    function call()
+    {
+        $arguments = func_get_args();
+        $id = $this->_makeId($arguments);
+        $data = $this->get($id, $this->_defaultGroup);
+        if ($data !== false) {
+            if ($this->_debugCacheLiteFunction) {
+                echo "Cache hit !\n";
+            }
+            $array = unserialize($data);
+            $output = $array['output'];
+            $result = $array['result'];
+        } else {
+            if ($this->_debugCacheLiteFunction) {
+                echo "Cache missed !\n";
+            }
+            ob_start();
+            ob_implicit_flush(false);
+            $target = array_shift($arguments);
+            if (is_array($target)) {
+                // in this case, $target is for example array($obj, 'method')
+                $object = $target[0];
+                $method = $target[1];
+                $result = call_user_func_array(array(&$object, $method), $arguments);
+            } else {
+                if (strstr($target, '::')) { // classname::staticMethod
+                    list($class, $method) = explode('::', $target);
+                    $result = call_user_func_array(array($class, $method), $arguments);
+                } else if (strstr($target, '->')) { // object->method
+                    // use a stupid name ($objet_123456789 because) of problems where the object
+                    // name is the same as this var name
+                    list($object_123456789, $method) = explode('->', $target);
+                    global $$object_123456789;
+                    $result = call_user_func_array(array($$object_123456789, $method), $arguments);
+                } else { // function
+                    $result = call_user_func_array($target, $arguments);
+                }
+            }
+            $output = ob_get_contents();
+            ob_end_clean();
+            if ($this->_dontCacheWhenTheResultIsFalse) {
+                if ((is_bool($result)) && (!($result))) {
+                    echo($output);
+                    return $result;
+                }
+            }
+            if ($this->_dontCacheWhenTheResultIsNull) {
+                if (is_null($result)) {
+                    echo($output);
+                    return $result;
+                }
+            }
+            if ($this->_dontCacheWhenTheOutputContainsNOCACHE) {
+                if (strpos($output, 'NOCACHE') > -1) {
+                    return $result;
+                }
+            }
+            $array['output'] = $output;
+            $array['result'] = $result;
+            $this->save(serialize($array), $id, $this->_defaultGroup);
+        }
+        echo($output);
+        return $result;
+    }
+
+    /**
+    * Drop a cache file
+    *
+    * Arguments of this method are read with func_get_args. So it doesn't appear
+    * in the function definition. Synopsis :
+    * remove('functionName', $arg1, $arg2, ...)
+    * (arg1, arg2... are arguments of 'functionName')
+    *
+    * @return boolean true if no problem
+    * @access public
+    */
+    function drop()
+    {
+        $id = $this->_makeId(func_get_args());
+        return $this->remove($id, $this->_defaultGroup);
+    }
+
+    /**
+    * Make an id for the cache
+    *
+    * @var array result of func_get_args for the call() or the remove() method
+    * @return string id
+    * @access private
+    */
+    function _makeId($arguments)
+    {
+        $id = serialize($arguments); // Generate a cache id
+        if (!$this->_fileNameProtection) {
+            $id = md5($id);
+            // if fileNameProtection is set to false, then the id has to be hashed
+            // because it's a very bad file name in most cases
+        }
+        return $id;
+    }
+
+}
+
+?>
diff --git a/lib/Cache/Lite/Output.php b/lib/Cache/Lite/Output.php
new file mode 100644 (file)
index 0000000..9732273
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+
+/**
+* This class extends Cache_Lite and uses output buffering to get the data to cache.
+*
+* There are some examples in the 'docs/examples' file
+* Technical choices are described in the 'docs/technical' file
+*
+* @package Cache_Lite
+* @version $Id: Output.php,v 1.4 2006/01/29 00:22:07 fab Exp $
+* @author Fabien MARTY <fab@php.net>
+*/
+
+require_once('Cache/Lite.php');
+
+class Cache_Lite_Output extends Cache_Lite
+{
+
+    // --- Public methods ---
+
+    /**
+    * Constructor
+    *
+    * $options is an assoc. To have a look at availables options,
+    * see the constructor of the Cache_Lite class in 'Cache_Lite.php'
+    *
+    * @param array $options options
+    * @access public
+    */
+    function Cache_Lite_Output($options)
+    {
+        $this->Cache_Lite($options);
+    }
+
+    /**
+    * Start the cache
+    *
+    * @param string $id cache id
+    * @param string $group name of the cache group
+    * @param boolean $doNotTestCacheValidity if set to true, the cache validity won't be tested
+    * @return boolean true if the cache is hit (false else)
+    * @access public
+    */
+    function start($id, $group = 'default', $doNotTestCacheValidity = false)
+    {
+        $data = $this->get($id, $group, $doNotTestCacheValidity);
+        if ($data !== false) {
+            echo($data);
+            return true;
+        }
+        ob_start();
+        ob_implicit_flush(false);
+        return false;
+    }
+
+    /**
+    * Stop the cache
+    *
+    * @access public
+    */
+    function end()
+    {
+        $data = ob_get_contents();
+        ob_end_clean();
+        $this->save($data, $this->_id, $this->_group);
+        echo($data);
+    }
+
+}
+
+
+?>
diff --git a/lib/HTTP/Request.php b/lib/HTTP/Request.php
new file mode 100644 (file)
index 0000000..55e227b
--- /dev/null
@@ -0,0 +1,1484 @@
+<?php
+/**
+ * Class for performing HTTP requests
+ *
+ * PHP versions 4 and 5
+ * 
+ * LICENSE:
+ *
+ * Copyright (c) 2002-2007, Richard Heyes
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * o Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * o The names of the authors may not be used to endorse or promote
+ *   products derived from this software without specific prior written
+ *   permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category    HTTP
+ * @package     HTTP_Request
+ * @author      Richard Heyes <richard@phpguru.org>
+ * @author      Alexey Borzov <avb@php.net>
+ * @copyright   2002-2007 Richard Heyes
+ * @license     http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version     CVS: $Id: Request.php 127 2008-01-17 20:21:37Z dcoulter $
+ * @link        http://pear.php.net/package/HTTP_Request/ 
+ */
+
+/**
+ * PEAR and PEAR_Error classes (for error handling)
+ */
+require_once 'PEAR.php';
+/**
+ * Socket class
+ */
+require_once 'Net/Socket.php';
+/**
+ * URL handling class
+ */ 
+require_once 'Net/URL.php';
+
+/**#@+
+ * Constants for HTTP request methods
+ */ 
+define('HTTP_REQUEST_METHOD_GET',     'GET',     true);
+define('HTTP_REQUEST_METHOD_HEAD',    'HEAD',    true);
+define('HTTP_REQUEST_METHOD_POST',    'POST',    true);
+define('HTTP_REQUEST_METHOD_PUT',     'PUT',     true);
+define('HTTP_REQUEST_METHOD_DELETE',  'DELETE',  true);
+define('HTTP_REQUEST_METHOD_OPTIONS', 'OPTIONS', true);
+define('HTTP_REQUEST_METHOD_TRACE',   'TRACE',   true);
+/**#@-*/
+
+/**#@+
+ * Constants for HTTP request error codes
+ */ 
+define('HTTP_REQUEST_ERROR_FILE',             1);
+define('HTTP_REQUEST_ERROR_URL',              2);
+define('HTTP_REQUEST_ERROR_PROXY',            4);
+define('HTTP_REQUEST_ERROR_REDIRECTS',        8);
+define('HTTP_REQUEST_ERROR_RESPONSE',        16);  
+define('HTTP_REQUEST_ERROR_GZIP_METHOD',     32);
+define('HTTP_REQUEST_ERROR_GZIP_READ',       64);
+define('HTTP_REQUEST_ERROR_GZIP_DATA',      128);
+define('HTTP_REQUEST_ERROR_GZIP_CRC',       256);
+/**#@-*/
+
+/**#@+
+ * Constants for HTTP protocol versions
+ */
+define('HTTP_REQUEST_HTTP_VER_1_0', '1.0', true);
+define('HTTP_REQUEST_HTTP_VER_1_1', '1.1', true);
+/**#@-*/
+
+if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) {
+   /**
+    * Whether string functions are overloaded by their mbstring equivalents 
+    */
+    define('HTTP_REQUEST_MBSTRING', true);
+} else {
+   /**
+    * @ignore
+    */
+    define('HTTP_REQUEST_MBSTRING', false);
+}
+
+/**
+ * Class for performing HTTP requests
+ *
+ * Simple example (fetches yahoo.com and displays it):
+ * <code>
+ * $a = &new HTTP_Request('http://www.yahoo.com/');
+ * $a->sendRequest();
+ * echo $a->getResponseBody();
+ * </code>
+ *
+ * @category    HTTP
+ * @package     HTTP_Request
+ * @author      Richard Heyes <richard@phpguru.org>
+ * @author      Alexey Borzov <avb@php.net>
+ * @version     Release: 1.4.2
+ */
+class HTTP_Request
+{
+   /**#@+
+    * @access private
+    */
+    /**
+    * Instance of Net_URL
+    * @var Net_URL
+    */
+    var $_url;
+
+    /**
+    * Type of request
+    * @var string
+    */
+    var $_method;
+
+    /**
+    * HTTP Version
+    * @var string
+    */
+    var $_http;
+
+    /**
+    * Request headers
+    * @var array
+    */
+    var $_requestHeaders;
+
+    /**
+    * Basic Auth Username
+    * @var string
+    */
+    var $_user;
+    
+    /**
+    * Basic Auth Password
+    * @var string
+    */
+    var $_pass;
+
+    /**
+    * Socket object
+    * @var Net_Socket
+    */
+    var $_sock;
+    
+    /**
+    * Proxy server
+    * @var string
+    */
+    var $_proxy_host;
+    
+    /**
+    * Proxy port
+    * @var integer
+    */
+    var $_proxy_port;
+    
+    /**
+    * Proxy username
+    * @var string
+    */
+    var $_proxy_user;
+    
+    /**
+    * Proxy password
+    * @var string
+    */
+    var $_proxy_pass;
+
+    /**
+    * Post data
+    * @var array
+    */
+    var $_postData;
+
+   /**
+    * Request body  
+    * @var string
+    */
+    var $_body;
+
+   /**
+    * A list of methods that MUST NOT have a request body, per RFC 2616
+    * @var array
+    */
+    var $_bodyDisallowed = array('TRACE');
+
+   /**
+    * Files to post 
+    * @var array
+    */
+    var $_postFiles = array();
+
+    /**
+    * Connection timeout.
+    * @var float
+    */
+    var $_timeout;
+    
+    /**
+    * HTTP_Response object
+    * @var HTTP_Response
+    */
+    var $_response;
+    
+    /**
+    * Whether to allow redirects
+    * @var boolean
+    */
+    var $_allowRedirects;
+    
+    /**
+    * Maximum redirects allowed
+    * @var integer
+    */
+    var $_maxRedirects;
+    
+    /**
+    * Current number of redirects
+    * @var integer
+    */
+    var $_redirects;
+
+   /**
+    * Whether to append brackets [] to array variables
+    * @var bool
+    */
+    var $_useBrackets = true;
+
+   /**
+    * Attached listeners
+    * @var array
+    */
+    var $_listeners = array();
+
+   /**
+    * Whether to save response body in response object property  
+    * @var bool
+    */
+    var $_saveBody = true;
+
+   /**
+    * Timeout for reading from socket (array(seconds, microseconds))
+    * @var array
+    */
+    var $_readTimeout = null;
+
+   /**
+    * Options to pass to Net_Socket::connect. See stream_context_create
+    * @var array
+    */
+    var $_socketOptions = null;
+   /**#@-*/
+
+    /**
+    * Constructor
+    *
+    * Sets up the object
+    * @param    string  The url to fetch/access
+    * @param    array   Associative array of parameters which can have the following keys:
+    * <ul>
+    *   <li>method         - Method to use, GET, POST etc (string)</li>
+    *   <li>http           - HTTP Version to use, 1.0 or 1.1 (string)</li>
+    *   <li>user           - Basic Auth username (string)</li>
+    *   <li>pass           - Basic Auth password (string)</li>
+    *   <li>proxy_host     - Proxy server host (string)</li>
+    *   <li>proxy_port     - Proxy server port (integer)</li>
+    *   <li>proxy_user     - Proxy auth username (string)</li>
+    *   <li>proxy_pass     - Proxy auth password (string)</li>
+    *   <li>timeout        - Connection timeout in seconds (float)</li>
+    *   <li>allowRedirects - Whether to follow redirects or not (bool)</li>
+    *   <li>maxRedirects   - Max number of redirects to follow (integer)</li>
+    *   <li>useBrackets    - Whether to append [] to array variable names (bool)</li>
+    *   <li>saveBody       - Whether to save response body in response object property (bool)</li>
+    *   <li>readTimeout    - Timeout for reading / writing data over the socket (array (seconds, microseconds))</li>
+    *   <li>socketOptions  - Options to pass to Net_Socket object (array)</li>
+    * </ul>
+    * @access public
+    */
+    function HTTP_Request($url = '', $params = array())
+    {
+        $this->_method         =  HTTP_REQUEST_METHOD_GET;
+        $this->_http           =  HTTP_REQUEST_HTTP_VER_1_1;
+        $this->_requestHeaders = array();
+        $this->_postData       = array();
+        $this->_body           = null;
+
+        $this->_user = null;
+        $this->_pass = null;
+
+        $this->_proxy_host = null;
+        $this->_proxy_port = null;
+        $this->_proxy_user = null;
+        $this->_proxy_pass = null;
+
+        $this->_allowRedirects = false;
+        $this->_maxRedirects   = 3;
+        $this->_redirects      = 0;
+
+        $this->_timeout  = null;
+        $this->_response = null;
+
+        foreach ($params as $key => $value) {
+            $this->{'_' . $key} = $value;
+        }
+
+        if (!empty($url)) {
+            $this->setURL($url);
+        }
+
+        // Default useragent
+        $this->addHeader('User-Agent', 'PEAR HTTP_Request class ( http://pear.php.net/ )');
+
+        // We don't do keep-alives by default
+        $this->addHeader('Connection', 'close');
+
+        // Basic authentication
+        if (!empty($this->_user)) {
+            $this->addHeader('Authorization', 'Basic ' . base64_encode($this->_user . ':' . $this->_pass));
+        }
+
+        // Proxy authentication (see bug #5913)
+        if (!empty($this->_proxy_user)) {
+            $this->addHeader('Proxy-Authorization', 'Basic ' . base64_encode($this->_proxy_user . ':' . $this->_proxy_pass));
+        }
+
+        // Use gzip encoding if possible
+        if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http && extension_loaded('zlib')) {
+            $this->addHeader('Accept-Encoding', 'gzip');
+        }
+    }
+    
+    /**
+    * Generates a Host header for HTTP/1.1 requests
+    *
+    * @access private
+    * @return string
+    */
+    function _generateHostHeader()
+    {
+        if ($this->_url->port != 80 AND strcasecmp($this->_url->protocol, 'http') == 0) {
+            $host = $this->_url->host . ':' . $this->_url->port;
+
+        } elseif ($this->_url->port != 443 AND strcasecmp($this->_url->protocol, 'https') == 0) {
+            $host = $this->_url->host . ':' . $this->_url->port;
+
+        } elseif ($this->_url->port == 443 AND strcasecmp($this->_url->protocol, 'https') == 0 AND strpos($this->_url->url, ':443') !== false) {
+            $host = $this->_url->host . ':' . $this->_url->port;
+        
+        } else {
+            $host = $this->_url->host;
+        }
+
+        return $host;
+    }
+    
+    /**
+    * Resets the object to its initial state (DEPRECATED).
+    * Takes the same parameters as the constructor.
+    *
+    * @param  string $url    The url to be requested
+    * @param  array  $params Associative array of parameters
+    *                        (see constructor for details)
+    * @access public
+    * @deprecated deprecated since 1.2, call the constructor if this is necessary
+    */
+    function reset($url, $params = array())
+    {
+        $this->HTTP_Request($url, $params);
+    }
+
+    /**
+    * Sets the URL to be requested
+    *
+    * @param  string The url to be requested
+    * @access public
+    */
+    function setURL($url)
+    {
+        $this->_url = &new Net_URL($url, $this->_useBrackets);
+
+        if (!empty($this->_url->user) || !empty($this->_url->pass)) {
+            $this->setBasicAuth($this->_url->user, $this->_url->pass);
+        }
+
+        if (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http) {
+            $this->addHeader('Host', $this->_generateHostHeader());
+        }
+
+        // set '/' instead of empty path rather than check later (see bug #8662)
+        if (empty($this->_url->path)) {
+            $this->_url->path = '/';
+        } 
+    }
+    
+   /**
+    * Returns the current request URL  
+    *
+    * @return   string  Current request URL
+    * @access   public
+    */
+    function getUrl()
+    {
+        return empty($this->_url)? '': $this->_url->getUrl();
+    }
+
+    /**
+    * Sets a proxy to be used
+    *
+    * @param string     Proxy host
+    * @param int        Proxy port
+    * @param string     Proxy username
+    * @param string     Proxy password
+    * @access public
+    */
+    function setProxy($host, $port = 8080, $user = null, $pass = null)
+    {
+        $this->_proxy_host = $host;
+        $this->_proxy_port = $port;
+        $this->_proxy_user = $user;
+        $this->_proxy_pass = $pass;
+
+        if (!empty($user)) {
+            $this->addHeader('Proxy-Authorization', 'Basic ' . base64_encode($user . ':' . $pass));
+        }
+    }
+
+    /**
+    * Sets basic authentication parameters
+    *
+    * @param string     Username
+    * @param string     Password
+    */
+    function setBasicAuth($user, $pass)
+    {
+        $this->_user = $user;
+        $this->_pass = $pass;
+
+        $this->addHeader('Authorization', 'Basic ' . base64_encode($user . ':' . $pass));
+    }
+
+    /**
+    * Sets the method to be used, GET, POST etc.
+    *
+    * @param string     Method to use. Use the defined constants for this
+    * @access public
+    */
+    function setMethod($method)
+    {
+        $this->_method = $method;
+    }
+
+    /**
+    * Sets the HTTP version to use, 1.0 or 1.1
+    *
+    * @param string     Version to use. Use the defined constants for this
+    * @access public
+    */
+    function setHttpVer($http)
+    {
+        $this->_http = $http;
+    }
+
+    /**
+    * Adds a request header
+    *
+    * @param string     Header name
+    * @param string     Header value
+    * @access public
+    */
+    function addHeader($name, $value)
+    {
+        $this->_requestHeaders[strtolower($name)] = $value;
+    }
+
+    /**
+    * Removes a request header
+    *
+    * @param string     Header name to remove
+    * @access public
+    */
+    function removeHeader($name)
+    {
+        if (isset($this->_requestHeaders[strtolower($name)])) {
+            unset($this->_requestHeaders[strtolower($name)]);
+        }
+    }
+
+    /**
+    * Adds a querystring parameter
+    *
+    * @param string     Querystring parameter name
+    * @param string     Querystring parameter value
+    * @param bool       Whether the value is already urlencoded or not, default = not
+    * @access public
+    */
+    function addQueryString($name, $value, $preencoded = false)
+    {
+        $this->_url->addQueryString($name, $value, $preencoded);
+    }    
+    
+    /**
+    * Sets the querystring to literally what you supply
+    *
+    * @param string     The querystring data. Should be of the format foo=bar&x=y etc
+    * @param bool       Whether data is already urlencoded or not, default = already encoded
+    * @access public
+    */
+    function addRawQueryString($querystring, $preencoded = true)
+    {
+        $this->_url->addRawQueryString($querystring, $preencoded);
+    }
+
+    /**
+    * Adds postdata items
+    *
+    * @param string     Post data name
+    * @param string     Post data value
+    * @param bool       Whether data is already urlencoded or not, default = not
+    * @access public
+    */
+    function addPostData($name, $value, $preencoded = false)
+    {
+        if ($preencoded) {
+            $this->_postData[$name] = $value;
+        } else {
+            $this->_postData[$name] = $this->_arrayMapRecursive('urlencode', $value);
+        }
+    }
+
+   /**
+    * Recursively applies the callback function to the value
+    * 
+    * @param    mixed   Callback function
+    * @param    mixed   Value to process
+    * @access   private
+    * @return   mixed   Processed value
+    */
+    function _arrayMapRecursive($callback, $value)
+    {
+        if (!is_array($value)) {
+            return call_user_func($callback, $value);
+        } else {
+            $map = array();
+            foreach ($value as $k => $v) {
+                $map[$k] = $this->_arrayMapRecursive($callback, $v);
+            }
+            return $map;
+        }
+    }
+
+   /**
+    * Adds a file to upload
+    * 
+    * This also changes content-type to 'multipart/form-data' for proper upload
+    * 
+    * @access public
+    * @param  string    name of file-upload field
+    * @param  mixed     file name(s)
+    * @param  mixed     content-type(s) of file(s) being uploaded
+    * @return bool      true on success
+    * @throws PEAR_Error
+    */
+    function addFile($inputName, $fileName, $contentType = 'application/octet-stream')
+    {
+        if (!is_array($fileName) && !is_readable($fileName)) {
+            return PEAR::raiseError("File '{$fileName}' is not readable", HTTP_REQUEST_ERROR_FILE);
+        } elseif (is_array($fileName)) {
+            foreach ($fileName as $name) {
+                if (!is_readable($name)) {
+                    return PEAR::raiseError("File '{$name}' is not readable", HTTP_REQUEST_ERROR_FILE);
+                }
+            }
+        }
+        $this->addHeader('Content-Type', 'multipart/form-data');
+        $this->_postFiles[$inputName] = array(
+            'name' => $fileName,
+            'type' => $contentType
+        );
+        return true;
+    }
+
+    /**
+    * Adds raw postdata (DEPRECATED)
+    *
+    * @param string     The data
+    * @param bool       Whether data is preencoded or not, default = already encoded
+    * @access public
+    * @deprecated       deprecated since 1.3.0, method setBody() should be used instead
+    */
+    function addRawPostData($postdata, $preencoded = true)
+    {
+        $this->_body = $preencoded ? $postdata : urlencode($postdata);
+    }
+
+   /**
+    * Sets the request body (for POST, PUT and similar requests)
+    *
+    * @param    string  Request body
+    * @access   public
+    */
+    function setBody($body)
+    {
+        $this->_body = $body;
+    }
+
+    /**
+    * Clears any postdata that has been added (DEPRECATED). 
+    * 
+    * Useful for multiple request scenarios.
+    *
+    * @access public
+    * @deprecated deprecated since 1.2
+    */
+    function clearPostData()
+    {
+        $this->_postData = null;
+    }
+
+    /**
+    * Appends a cookie to "Cookie:" header
+    * 
+    * @param string $name cookie name
+    * @param string $value cookie value
+    * @access public
+    */
+    function addCookie($name, $value)
+    {
+        $cookies = isset($this->_requestHeaders['cookie']) ? $this->_requestHeaders['cookie']. '; ' : '';
+        $this->addHeader('Cookie', $cookies . $name . '=' . $value);
+    }
+    
+    /**
+    * Clears any cookies that have been added (DEPRECATED). 
+    * 
+    * Useful for multiple request scenarios
+    *
+    * @access public
+    * @deprecated deprecated since 1.2
+    */
+    function clearCookies()
+    {
+        $this->removeHeader('Cookie');
+    }
+
+    /**
+    * Sends the request
+    *
+    * @access public
+    * @param  bool   Whether to store response body in Response object property,
+    *                set this to false if downloading a LARGE file and using a Listener
+    * @return mixed  PEAR error on error, true otherwise
+    */
+    function sendRequest($saveBody = true)
+    {
+        if (!is_a($this->_url, 'Net_URL')) {
+            return PEAR::raiseError('No URL given', HTTP_REQUEST_ERROR_URL);
+        }
+
+        $host = isset($this->_proxy_host) ? $this->_proxy_host : $this->_url->host;
+        $port = isset($this->_proxy_port) ? $this->_proxy_port : $this->_url->port;
+
+        // 4.3.0 supports SSL connections using OpenSSL. The function test determines
+        // we running on at least 4.3.0
+        if (strcasecmp($this->_url->protocol, 'https') == 0 AND function_exists('file_get_contents') AND extension_loaded('openssl')) {
+            if (isset($this->_proxy_host)) {
+                return PEAR::raiseError('HTTPS proxies are not supported', HTTP_REQUEST_ERROR_PROXY);
+            }
+            $host = 'ssl://' . $host;
+        }
+
+        // magic quotes may fuck up file uploads and chunked response processing
+        $magicQuotes = ini_get('magic_quotes_runtime');
+        ini_set('magic_quotes_runtime', false);
+
+        // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive 
+        // connection token to a proxy server...
+        if (isset($this->_proxy_host) && !empty($this->_requestHeaders['connection']) &&
+            'Keep-Alive' == $this->_requestHeaders['connection'])
+        {
+            $this->removeHeader('connection');
+        }
+
+        $keepAlive = (HTTP_REQUEST_HTTP_VER_1_1 == $this->_http && empty($this->_requestHeaders['connection'])) ||
+                     (!empty($this->_requestHeaders['connection']) && 'Keep-Alive' == $this->_requestHeaders['connection']);
+        $sockets   = &PEAR::getStaticProperty('HTTP_Request', 'sockets');
+        $sockKey   = $host . ':' . $port;
+        unset($this->_sock);
+
+        // There is a connected socket in the "static" property?
+        if ($keepAlive && !empty($sockets[$sockKey]) &&
+            !empty($sockets[$sockKey]->fp)) 
+        {
+            $this->_sock =& $sockets[$sockKey];
+            $err = null;
+        } else {
+            $this->_notify('connect');
+            $this->_sock =& new Net_Socket();
+            $err = $this->_sock->connect($host, $port, null, $this->_timeout, $this->_socketOptions);
+        }
+        PEAR::isError($err) or $err = $this->_sock->write($this->_buildRequest());
+
+        if (!PEAR::isError($err)) {
+            if (!empty($this->_readTimeout)) {
+                $this->_sock->setTimeout($this->_readTimeout[0], $this->_readTimeout[1]);
+            }
+
+            $this->_notify('sentRequest');
+
+            // Read the response
+            $this->_response = &new HTTP_Response($this->_sock, $this->_listeners);
+            $err = $this->_response->process(
+                $this->_saveBody && $saveBody,
+                HTTP_REQUEST_METHOD_HEAD != $this->_method
+            );
+
+            if ($keepAlive) {
+                $keepAlive = (isset($this->_response->_headers['content-length'])
+                              || (isset($this->_response->_headers['transfer-encoding'])
+                                  && strtolower($this->_response->_headers['transfer-encoding']) == 'chunked'));
+                if ($keepAlive) {
+                    if (isset($this->_response->_headers['connection'])) {
+                        $keepAlive = strtolower($this->_response->_headers['connection']) == 'keep-alive';
+                    } else {
+                        $keepAlive = 'HTTP/'.HTTP_REQUEST_HTTP_VER_1_1 == $this->_response->_protocol;
+                    }
+                }
+            }
+        }
+
+        ini_set('magic_quotes_runtime', $magicQuotes);
+
+        if (PEAR::isError($err)) {
+            return $err;
+        }
+
+        if (!$keepAlive) {
+            $this->disconnect();
+        // Store the connected socket in "static" property
+        } elseif (empty($sockets[$sockKey]) || empty($sockets[$sockKey]->fp)) {
+            $sockets[$sockKey] =& $this->_sock;
+        }
+
+        // Check for redirection
+        if (    $this->_allowRedirects
+            AND $this->_redirects <= $this->_maxRedirects
+            AND $this->getResponseCode() > 300
+            AND $this->getResponseCode() < 399
+            AND !empty($this->_response->_headers['location'])) {
+
+            
+            $redirect = $this->_response->_headers['location'];
+
+            // Absolute URL
+            if (preg_match('/^https?:\/\//i', $redirect)) {
+                $this->_url = &new Net_URL($redirect);
+                $this->addHeader('Host', $this->_generateHostHeader());
+            // Absolute path
+            } elseif ($redirect{0} == '/') {
+                $this->_url->path = $redirect;
+            
+            // Relative path
+            } elseif (substr($redirect, 0, 3) == '../' OR substr($redirect, 0, 2) == './') {
+                if (substr($this->_url->path, -1) == '/') {
+                    $redirect = $this->_url->path . $redirect;
+                } else {
+                    $redirect = dirname($this->_url->path) . '/' . $redirect;
+                }
+                $redirect = Net_URL::resolvePath($redirect);
+                $this->_url->path = $redirect;
+                
+            // Filename, no path
+            } else {
+                if (substr($this->_url->path, -1) == '/') {
+                    $redirect = $this->_url->path . $redirect;
+                } else {
+                    $redirect = dirname($this->_url->path) . '/' . $redirect;
+                }
+                $this->_url->path = $redirect;
+            }
+
+            $this->_redirects++;
+            return $this->sendRequest($saveBody);
+
+        // Too many redirects
+        } elseif ($this->_allowRedirects AND $this->_redirects > $this->_maxRedirects) {
+            return PEAR::raiseError('Too many redirects', HTTP_REQUEST_ERROR_REDIRECTS);
+        }
+
+        return true;
+    }
+
+    /**
+     * Disconnect the socket, if connected. Only useful if using Keep-Alive.
+     *
+     * @access public
+     */
+    function disconnect()
+    {
+        if (!empty($this->_sock) && !empty($this->_sock->fp)) {
+            $this->_notify('disconnect');
+            $this->_sock->disconnect();
+        }
+    }
+
+    /**
+    * Returns the response code
+    *
+    * @access public
+    * @return mixed     Response code, false if not set
+    */
+    function getResponseCode()
+    {
+        return isset($this->_response->_code) ? $this->_response->_code : false;
+    }
+
+    /**
+    * Returns either the named header or all if no name given
+    *
+    * @access public
+    * @param string     The header name to return, do not set to get all headers
+    * @return mixed     either the value of $headername (false if header is not present)
+    *                   or an array of all headers
+    */
+    function getResponseHeader($headername = null)
+    {
+        if (!isset($headername)) {
+            return isset($this->_response->_headers)? $this->_response->_headers: array();
+        } else {
+            $headername = strtolower($headername);
+            return isset($this->_response->_headers[$headername]) ? $this->_response->_headers[$headername] : false;
+        }
+    }
+
+    /**
+    * Returns the body of the response
+    *
+    * @access public
+    * @return mixed     response body, false if not set
+    */
+    function getResponseBody()
+    {
+        return isset($this->_response->_body) ? $this->_response->_body : false;
+    }
+
+    /**
+    * Returns cookies set in response
+    * 
+    * @access public
+    * @return mixed     array of response cookies, false if none are present
+    */
+    function getResponseCookies()
+    {
+        return isset($this->_response->_cookies) ? $this->_response->_cookies : false;
+    }
+
+    /**
+    * Builds the request string
+    *
+    * @access private
+    * @return string The request string
+    */
+    function _buildRequest()
+    {
+        $separator = ini_get('arg_separator.output');
+        ini_set('arg_separator.output', '&');
+        $querystring = ($querystring = $this->_url->getQueryString()) ? '?' . $querystring : '';
+        ini_set('arg_separator.output', $separator);
+
+        $host = isset($this->_proxy_host) ? $this->_url->protocol . '://' . $this->_url->host : '';
+        $port = (isset($this->_proxy_host) AND $this->_url->port != 80) ? ':' . $this->_url->port : '';
+        $path = $this->_url->path . $querystring;
+        $url  = $host . $port . $path;
+
+        if (!strlen($url)) {
+            $url = '/';
+        }
+
+        $request = $this->_method . ' ' . $url . ' HTTP/' . $this->_http . "\r\n";
+
+        if (in_array($this->_method, $this->_bodyDisallowed) ||
+            (0 == strlen($this->_body) && (HTTP_REQUEST_METHOD_POST != $this->_method ||
+             (empty($this->_postData) && empty($this->_postFiles)))))
+        {
+            $this->removeHeader('Content-Type');
+        } else {
+            if (empty($this->_requestHeaders['content-type'])) {
+                // Add default content-type
+                $this->addHeader('Content-Type', 'application/x-www-form-urlencoded');
+            } elseif ('multipart/form-data' == $this->_requestHeaders['content-type']) {
+                $boundary = 'HTTP_Request_' . md5(uniqid('request') . microtime());
+                $this->addHeader('Content-Type', 'multipart/form-data; boundary=' . $boundary);
+            }
+        }
+
+        // Request Headers
+        if (!empty($this->_requestHeaders)) {
+            foreach ($this->_requestHeaders as $name => $value) {
+                $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));
+                $request      .= $canonicalName . ': ' . $value . "\r\n";
+            }
+        }
+
+        // No post data or wrong method, so simply add a final CRLF
+        if (in_array($this->_method, $this->_bodyDisallowed) || 
+            (HTTP_REQUEST_METHOD_POST != $this->_method && 0 == strlen($this->_body))) {
+
+            $request .= "\r\n";
+
+        // Post data if it's an array
+        } elseif (HTTP_REQUEST_METHOD_POST == $this->_method && 
+                  (!empty($this->_postData) || !empty($this->_postFiles))) {
+
+            // "normal" POST request
+            if (!isset($boundary)) {
+                $postdata = implode('&', array_map(
+                    create_function('$a', 'return $a[0] . \'=\' . $a[1];'), 
+                    $this->_flattenArray('', $this->_postData)
+                ));
+
+            // multipart request, probably with file uploads
+            } else {
+                $postdata = '';
+                if (!empty($this->_postData)) {
+                    $flatData = $this->_flattenArray('', $this->_postData);
+                    foreach ($flatData as $item) {
+                        $postdata .= '--' . $boundary . "\r\n";
+                        $postdata .= 'Content-Disposition: form-data; name="' . $item[0] . '"';
+                        $postdata .= "\r\n\r\n" . urldecode($item[1]) . "\r\n";
+                    }
+                }
+                foreach ($this->_postFiles as $name => $value) {
+                    if (is_array($value['name'])) {
+                        $varname       = $name . ($this->_useBrackets? '[]': '');
+                    } else {
+                        $varname       = $name;
+                        $value['name'] = array($value['name']);
+                    }
+                    foreach ($value['name'] as $key => $filename) {
+                        $fp   = fopen($filename, 'r');
+                        $data = fread($fp, filesize($filename));
+                        fclose($fp);
+                        $basename = basename($filename);
+                        $type     = is_array($value['type'])? @$value['type'][$key]: $value['type'];
+
+                        $postdata .= '--' . $boundary . "\r\n";
+                        $postdata .= 'Content-Disposition: form-data; name="' . $varname . '"; filename="' . $basename . '"';
+                        $postdata .= "\r\nContent-Type: " . $type;
+                        $postdata .= "\r\n\r\n" . $data . "\r\n";
+                    }
+                }
+                $postdata .= '--' . $boundary . "--\r\n";
+            }
+            $request .= 'Content-Length: ' .
+                        (HTTP_REQUEST_MBSTRING? mb_strlen($postdata, 'iso-8859-1'): strlen($postdata)) .
+                        "\r\n\r\n";
+            $request .= $postdata;
+
+        // Explicitly set request body
+        } elseif (0 < strlen($this->_body)) {
+
+            $request .= 'Content-Length: ' .
+                        (HTTP_REQUEST_MBSTRING? mb_strlen($this->_body, 'iso-8859-1'): strlen($this->_body)) .
+                        "\r\n\r\n";
+            $request .= $this->_body;
+
+        // Terminate headers with CRLF on POST request with no body, too
+        } else {
+
+            $request .= "\r\n";
+        }
+        
+        return $request;
+    }
+
+   /**
+    * Helper function to change the (probably multidimensional) associative array
+    * into the simple one.
+    *
+    * @param    string  name for item
+    * @param    mixed   item's values
+    * @return   array   array with the following items: array('item name', 'item value');
+    * @access   private
+    */
+    function _flattenArray($name, $values)
+    {
+        if (!is_array($values)) {
+            return array(array($name, $values));
+        } else {
+            $ret = array();
+            foreach ($values as $k => $v) {
+                if (empty($name)) {
+                    $newName = $k;
+                } elseif ($this->_useBrackets) {
+                    $newName = $name . '[' . $k . ']';
+                } else {
+                    $newName = $name;
+                }
+                $ret = array_merge($ret, $this->_flattenArray($newName, $v));
+            }
+            return $ret;
+        }
+    }
+
+
+   /**
+    * Adds a Listener to the list of listeners that are notified of
+    * the object's events
+    * 
+    * Events sent by HTTP_Request object
+    * - 'connect': on connection to server
+    * - 'sentRequest': after the request was sent
+    * - 'disconnect': on disconnection from server
+    *
+    * Events sent by HTTP_Response object
+    * - 'gotHeaders': after receiving response headers (headers are passed in $data)
+    * - 'tick': on receiving a part of response body (the part is passed in $data)
+    * - 'gzTick': on receiving a gzip-encoded part of response body (ditto)
+    * - 'gotBody': after receiving the response body (passes the decoded body in $data if it was gzipped)
+    *
+    * @param    HTTP_Request_Listener   listener to attach
+    * @return   boolean                 whether the listener was successfully attached
+    * @access   public
+    */
+    function attach(&$listener)
+    {
+        if (!is_a($listener, 'HTTP_Request_Listener')) {
+            return false;
+        }
+        $this->_listeners[$listener->getId()] =& $listener;
+        return true;
+    }
+
+
+   /**
+    * Removes a Listener from the list of listeners 
+    * 
+    * @param    HTTP_Request_Listener   listener to detach
+    * @return   boolean                 whether the listener was successfully detached
+    * @access   public
+    */
+    function detach(&$listener)
+    {
+        if (!is_a($listener, 'HTTP_Request_Listener') || 
+            !isset($this->_listeners[$listener->getId()])) {
+            return false;
+        }
+        unset($this->_listeners[$listener->getId()]);
+        return true;
+    }
+
+
+   /**
+    * Notifies all registered listeners of an event.
+    * 
+    * @param    string  Event name
+    * @param    mixed   Additional data
+    * @access   private
+    * @see      HTTP_Request::attach()
+    */
+    function _notify($event, $data = null)
+    {
+        foreach (array_keys($this->_listeners) as $id) {
+            $this->_listeners[$id]->update($this, $event, $data);
+        }
+    }
+}
+
+
+/**
+ * Response class to complement the Request class
+ *
+ * @category    HTTP
+ * @package     HTTP_Request
+ * @author      Richard Heyes <richard@phpguru.org>
+ * @author      Alexey Borzov <avb@php.net>
+ * @version     Release: 1.4.2
+ */
+class HTTP_Response
+{
+    /**
+    * Socket object
+    * @var Net_Socket
+    */
+    var $_sock;
+
+    /**
+    * Protocol
+    * @var string
+    */
+    var $_protocol;
+    
+    /**
+    * Return code
+    * @var string
+    */
+    var $_code;
+    
+    /**
+    * Response headers
+    * @var array
+    */
+    var $_headers;
+
+    /**
+    * Cookies set in response  
+    * @var array
+    */
+    var $_cookies;
+
+    /**
+    * Response body
+    * @var string
+    */
+    var $_body = '';
+
+   /**
+    * Used by _readChunked(): remaining length of the current chunk
+    * @var string
+    */
+    var $_chunkLength = 0;
+
+   /**
+    * Attached listeners
+    * @var array
+    */
+    var $_listeners = array();
+
+   /**
+    * Bytes left to read from message-body
+    * @var null|int
+    */
+    var $_toRead;
+
+    /**
+    * Constructor
+    *
+    * @param  Net_Socket    socket to read the response from
+    * @param  array         listeners attached to request
+    */
+    function HTTP_Response(&$sock, &$listeners)
+    {
+        $this->_sock      =& $sock;
+        $this->_listeners =& $listeners;
+    }
+
+
+   /**
+    * Processes a HTTP response
+    * 
+    * This extracts response code, headers, cookies and decodes body if it 
+    * was encoded in some way
+    *
+    * @access public
+    * @param  bool      Whether to store response body in object property, set
+    *                   this to false if downloading a LARGE file and using a Listener.
+    *                   This is assumed to be true if body is gzip-encoded.
+    * @param  bool      Whether the response can actually have a message-body.
+    *                   Will be set to false for HEAD requests.
+    * @throws PEAR_Error
+    * @return mixed     true on success, PEAR_Error in case of malformed response
+    */
+    function process($saveBody = true, $canHaveBody = true)
+    {
+        do {
+            $line = $this->_sock->readLine();
+            if (sscanf($line, 'HTTP/%s %s', $http_version, $returncode) != 2) {
+                return PEAR::raiseError('Malformed response', HTTP_REQUEST_ERROR_RESPONSE);
+            } else {
+                $this->_protocol = 'HTTP/' . $http_version;
+                $this->_code     = intval($returncode);
+            }
+            while ('' !== ($header = $this->_sock->readLine())) {
+                $this->_processHeader($header);
+            }
+        } while (100 == $this->_code);
+
+        $this->_notify('gotHeaders', $this->_headers);
+
+        // RFC 2616, section 4.4:
+        // 1. Any response message which "MUST NOT" include a message-body ... 
+        // is always terminated by the first empty line after the header fields 
+        // 3. ... If a message is received with both a
+        // Transfer-Encoding header field and a Content-Length header field,
+        // the latter MUST be ignored.
+        $canHaveBody = $canHaveBody && $this->_code >= 200 && 
+                       $this->_code != 204 && $this->_code != 304;
+
+        // If response body is present, read it and decode
+        $chunked = isset($this->_headers['transfer-encoding']) && ('chunked' == $this->_headers['transfer-encoding']);
+        $gzipped = isset($this->_headers['content-encoding']) && ('gzip' == $this->_headers['content-encoding']);
+        $hasBody = false;
+        if ($canHaveBody && ($chunked || !isset($this->_headers['content-length']) || 
+                0 != $this->_headers['content-length']))
+        {
+            if ($chunked || !isset($this->_headers['content-length'])) {
+                $this->_toRead = null;
+            } else {
+                $this->_toRead = $this->_headers['content-length'];
+            }
+            while (!$this->_sock->eof() && (is_null($this->_toRead) || 0 < $this->_toRead)) {
+                if ($chunked) {
+                    $data = $this->_readChunked();
+                } elseif (is_null($this->_toRead)) {
+                    $data = $this->_sock->read(4096);
+                } else {
+                    $data = $this->_sock->read(min(4096, $this->_toRead));
+                    $this->_toRead -= HTTP_REQUEST_MBSTRING? mb_strlen($data, 'iso-8859-1'): strlen($data);
+                }
+                if ('' == $data) {
+                    break;
+                } else {
+                    $hasBody = true;
+                    if ($saveBody || $gzipped) {
+                        $this->_body .= $data;
+                    }
+                    $this->_notify($gzipped? 'gzTick': 'tick', $data);
+                }
+            }
+        }
+
+        if ($hasBody) {
+            // Uncompress the body if needed
+            if ($gzipped) {
+                $body = $this->_decodeGzip($this->_body);
+                if (PEAR::isError($body)) {
+                    return $body;
+                }
+                $this->_body = $body;
+                $this->_notify('gotBody', $this->_body);
+            } else {
+                $this->_notify('gotBody');
+            }
+        }
+        return true;
+    }
+
+
+   /**
+    * Processes the response header
+    *
+    * @access private
+    * @param  string    HTTP header
+    */
+    function _processHeader($header)
+    {
+        if (false === strpos($header, ':')) {
+            return;
+        }
+        list($headername, $headervalue) = explode(':', $header, 2);
+        $headername  = strtolower($headername);
+        $headervalue = ltrim($headervalue);
+        
+        if ('set-cookie' != $headername) {
+            if (isset($this->_headers[$headername])) {
+                $this->_headers[$headername] .= ',' . $headervalue;
+            } else {
+                $this->_headers[$headername]  = $headervalue;
+            }
+        } else {
+            $this->_parseCookie($headervalue);
+        }
+    }
+
+
+   /**
+    * Parse a Set-Cookie header to fill $_cookies array
+    *
+    * @access private
+    * @param  string    value of Set-Cookie header
+    */
+    function _parseCookie($headervalue)
+    {
+        $cookie = array(
+            'expires' => null,
+            'domain'  => null,
+            'path'    => null,
+            'secure'  => false
+        );
+
+        // Only a name=value pair
+        if (!strpos($headervalue, ';')) {
+            $pos = strpos($headervalue, '=');
+            $cookie['name']  = trim(substr($headervalue, 0, $pos));
+            $cookie['value'] = trim(substr($headervalue, $pos + 1));
+
+        // Some optional parameters are supplied
+        } else {
+            $elements = explode(';', $headervalue);
+            $pos = strpos($elements[0], '=');
+            $cookie['name']  = trim(substr($elements[0], 0, $pos));
+            $cookie['value'] = trim(substr($elements[0], $pos + 1));
+
+            for ($i = 1; $i < count($elements); $i++) {
+                if (false === strpos($elements[$i], '=')) {
+                    $elName  = trim($elements[$i]);
+                    $elValue = null;
+                } else {
+                    list ($elName, $elValue) = array_map('trim', explode('=', $elements[$i]));
+                }
+                $elName = strtolower($elName);
+                if ('secure' == $elName) {
+                    $cookie['secure'] = true;
+                } elseif ('expires' == $elName) {
+                    $cookie['expires'] = str_replace('"', '', $elValue);
+                } elseif ('path' == $elName || 'domain' == $elName) {
+                    $cookie[$elName] = urldecode($elValue);
+                } else {
+                    $cookie[$elName] = $elValue;
+                }
+            }
+        }
+        $this->_cookies[] = $cookie;
+    }
+
+
+   /**
+    * Read a part of response body encoded with chunked Transfer-Encoding
+    * 
+    * @access private
+    * @return string
+    */
+    function _readChunked()
+    {
+        // at start of the next chunk?
+        if (0 == $this->_chunkLength) {
+            $line = $this->_sock->readLine();
+            if (preg_match('/^([0-9a-f]+)/i', $line, $matches)) {
+                $this->_chunkLength = hexdec($matches[1]); 
+                // Chunk with zero length indicates the end
+                if (0 == $this->_chunkLength) {
+                    $this->_sock->readLine(); // make this an eof()
+                    return '';
+                }
+            } else {
+                return '';
+            }
+        }
+        $data = $this->_sock->read($this->_chunkLength);
+        $this->_chunkLength -= HTTP_REQUEST_MBSTRING? mb_strlen($data, 'iso-8859-1'): strlen($data);
+        if (0 == $this->_chunkLength) {
+            $this->_sock->readLine(); // Trailing CRLF
+        }
+        return $data;
+    }
+
+
+   /**
+    * Notifies all registered listeners of an event.
+    * 
+    * @param    string  Event name
+    * @param    mixed   Additional data
+    * @access   private
+    * @see HTTP_Request::_notify()
+    */
+    function _notify($event, $data = null)
+    {
+        foreach (array_keys($this->_listeners) as $id) {
+            $this->_listeners[$id]->update($this, $event, $data);
+        }
+    }
+
+
+   /**
+    * Decodes the message-body encoded by gzip
+    *
+    * The real decoding work is done by gzinflate() built-in function, this
+    * method only parses the header and checks data for compliance with
+    * RFC 1952  
+    *
+    * @access   private
+    * @param    string  gzip-encoded data
+    * @return   string  decoded data
+    */
+    function _decodeGzip($data)
+    {
+        if (HTTP_REQUEST_MBSTRING) {
+            $oldEncoding = mb_internal_encoding();
+            mb_internal_encoding('iso-8859-1');
+        }
+        $length = strlen($data);
+        // If it doesn't look like gzip-encoded data, don't bother
+        if (18 > $length || strcmp(substr($data, 0, 2), "\x1f\x8b")) {
+            return $data;
+        }
+        $method = ord(substr($data, 2, 1));
+        if (8 != $method) {
+            return PEAR::raiseError('_decodeGzip(): unknown compression method', HTTP_REQUEST_ERROR_GZIP_METHOD);
+        }
+        $flags = ord(substr($data, 3, 1));
+        if ($flags & 224) {
+            return PEAR::raiseError('_decodeGzip(): reserved bits are set', HTTP_REQUEST_ERROR_GZIP_DATA);
+        }
+
+        // header is 10 bytes minimum. may be longer, though.
+        $headerLength = 10;
+        // extra fields, need to skip 'em
+        if ($flags & 4) {
+            if ($length - $headerLength - 2 < 8) {
+                return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA);
+            }
+            $extraLength = unpack('v', substr($data, 10, 2));
+            if ($length - $headerLength - 2 - $extraLength[1] < 8) {
+                return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA);
+            }
+            $headerLength += $extraLength[1] + 2;
+        }
+        // file name, need to skip that
+        if ($flags & 8) {
+            if ($length - $headerLength - 1 < 8) {
+                return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA);
+            }
+            $filenameLength = strpos(substr($data, $headerLength), chr(0));
+            if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) {
+                return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA);
+            }
+            $headerLength += $filenameLength + 1;
+        }
+        // comment, need to skip that also
+        if ($flags & 16) {
+            if ($length - $headerLength - 1 < 8) {
+                return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA);
+            }
+            $commentLength = strpos(substr($data, $headerLength), chr(0));
+            if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) {
+                return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA);
+            }
+            $headerLength += $commentLength + 1;
+        }
+        // have a CRC for header. let's check
+        if ($flags & 1) {
+            if ($length - $headerLength - 2 < 8) {
+                return PEAR::raiseError('_decodeGzip(): data too short', HTTP_REQUEST_ERROR_GZIP_DATA);
+            }
+            $crcReal   = 0xffff & crc32(substr($data, 0, $headerLength));
+            $crcStored = unpack('v', substr($data, $headerLength, 2));
+            if ($crcReal != $crcStored[1]) {
+                return PEAR::raiseError('_decodeGzip(): header CRC check failed', HTTP_REQUEST_ERROR_GZIP_CRC);
+            }
+            $headerLength += 2;
+        }
+        // unpacked data CRC and size at the end of encoded data
+        $tmp = unpack('V2', substr($data, -8));
+        $dataCrc  = $tmp[1];
+        $dataSize = $tmp[2];
+
+        // finally, call the gzinflate() function
+        $unpacked = @gzinflate(substr($data, $headerLength, -8), $dataSize);
+        if (false === $unpacked) {
+            return PEAR::raiseError('_decodeGzip(): gzinflate() call failed', HTTP_REQUEST_ERROR_GZIP_READ);
+        } elseif ($dataSize != strlen($unpacked)) {
+            return PEAR::raiseError('_decodeGzip(): data size check failed', HTTP_REQUEST_ERROR_GZIP_READ);
+        } elseif ((0xffffffff & $dataCrc) != (0xffffffff & crc32($unpacked))) {
+            return PEAR::raiseError('_decodeGzip(): data CRC check failed', HTTP_REQUEST_ERROR_GZIP_CRC);
+        }
+        if (HTTP_REQUEST_MBSTRING) {
+            mb_internal_encoding($oldEncoding);
+        }
+        return $unpacked;
+    }
+} // End class HTTP_Response
+?>
diff --git a/lib/HTTP/Request/Listener.php b/lib/HTTP/Request/Listener.php
new file mode 100644 (file)
index 0000000..b005142
--- /dev/null
@@ -0,0 +1,106 @@
+<?php
+/**
+ * Listener for HTTP_Request and HTTP_Response objects
+ *
+ * PHP versions 4 and 5
+ * 
+ * LICENSE:
+ *
+ * Copyright (c) 2002-2007, Richard Heyes
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ * o Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ * o The names of the authors may not be used to endorse or promote
+ *   products derived from this software without specific prior written
+ *   permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category    HTTP
+ * @package     HTTP_Request
+ * @author      Alexey Borzov <avb@php.net>
+ * @copyright   2002-2007 Richard Heyes
+ * @license     http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version     CVS: $Id: Listener.php 127 2008-01-17 20:21:37Z dcoulter $
+ * @link        http://pear.php.net/package/HTTP_Request/ 
+ */
+
+/**
+ * Listener for HTTP_Request and HTTP_Response objects
+ *
+ * This class implements the Observer part of a Subject-Observer
+ * design pattern.
+ *
+ * @category    HTTP
+ * @package     HTTP_Request
+ * @author      Alexey Borzov <avb@php.net>
+ * @version     Release: 1.4.2
+ */
+class HTTP_Request_Listener 
+{
+   /**
+    * A listener's identifier
+    * @var string
+    */
+    var $_id;
+
+   /**
+    * Constructor, sets the object's identifier
+    *
+    * @access public
+    */
+    function HTTP_Request_Listener()
+    {
+        $this->_id = md5(uniqid('http_request_', 1));
+    }
+
+
+   /**
+    * Returns the listener's identifier
+    *
+    * @access public
+    * @return string
+    */
+    function getId()
+    {
+        return $this->_id;
+    }
+
+
+   /**
+    * This method is called when Listener is notified of an event
+    *
+    * @access   public
+    * @param    object  an object the listener is attached to
+    * @param    string  Event name
+    * @param    mixed   Additional data
+    * @abstract
+    */
+    function update(&$subject, $event, $data = null)
+    {
+        echo "Notified of event: '$event'\n";
+        if (null !== $data) {
+            echo "Additional data: ";
+            var_dump($data);
+        }
+    }
+}
+?>
diff --git a/lib/Net/Socket.php b/lib/Net/Socket.php
new file mode 100644 (file)
index 0000000..96f2c4b
--- /dev/null
@@ -0,0 +1,528 @@
+<?php
+//
+// +----------------------------------------------------------------------+
+// | PHP Version 4                                                        |
+// +----------------------------------------------------------------------+
+// | Copyright (c) 1997-2003 The PHP Group                                |
+// +----------------------------------------------------------------------+
+// | This source file is subject to version 2.0 of the PHP license,       |
+// | that is bundled with this package in the file LICENSE, and is        |
+// | available at through the world-wide-web at                           |
+// | http://www.php.net/license/2_02.txt.                                 |
+// | If you did not receive a copy of the PHP license and are unable to   |
+// | obtain it through the world-wide-web, please send a note to          |
+// | license@php.net so we can mail you a copy immediately.               |
+// +----------------------------------------------------------------------+
+// | Authors: Stig Bakken <ssb@php.net>                                   |
+// |          Chuck Hagenbuch <chuck@horde.org>                           |
+// +----------------------------------------------------------------------+
+//
+// $Id: Socket.php 32 2005-08-01 06:21:02Z dancoulter $
+
+require_once 'PEAR.php';
+
+define('NET_SOCKET_READ',  1);
+define('NET_SOCKET_WRITE', 2);
+define('NET_SOCKET_ERROR', 3);
+
+/**
+ * Generalized Socket class.
+ *
+ * @version 1.1
+ * @author Stig Bakken <ssb@php.net>
+ * @author Chuck Hagenbuch <chuck@horde.org>
+ */
+class Net_Socket extends PEAR {
+
+    /**
+     * Socket file pointer.
+     * @var resource $fp
+     */
+    var $fp = null;
+
+    /**
+     * Whether the socket is blocking. Defaults to true.
+     * @var boolean $blocking
+     */
+    var $blocking = true;
+
+    /**
+     * Whether the socket is persistent. Defaults to false.
+     * @var boolean $persistent
+     */
+    var $persistent = false;
+
+    /**
+     * The IP address to connect to.
+     * @var string $addr
+     */
+    var $addr = '';
+
+    /**
+     * The port number to connect to.
+     * @var integer $port
+     */
+    var $port = 0;
+
+    /**
+     * Number of seconds to wait on socket connections before assuming
+     * there's no more data. Defaults to no timeout.
+     * @var integer $timeout
+     */
+    var $timeout = false;
+
+    /**
+     * Number of bytes to read at a time in readLine() and
+     * readAll(). Defaults to 2048.
+     * @var integer $lineLength
+     */
+    var $lineLength = 2048;
+
+    /**
+     * Connect to the specified port. If called when the socket is
+     * already connected, it disconnects and connects again.
+     *
+     * @param string  $addr        IP address or host name.
+     * @param integer $port        TCP port number.
+     * @param boolean $persistent  (optional) Whether the connection is
+     *                             persistent (kept open between requests
+     *                             by the web server).
+     * @param integer $timeout     (optional) How long to wait for data.
+     * @param array   $options     See options for stream_context_create.
+     *
+     * @access public
+     *
+     * @return boolean | PEAR_Error  True on success or a PEAR_Error on failure.
+     */
+    function connect($addr, $port = 0, $persistent = null, $timeout = null, $options = null)
+    {
+        if (is_resource($this->fp)) {
+            @fclose($this->fp);
+            $this->fp = null;
+        }
+
+        if (!$addr) {
+            return $this->raiseError('$addr cannot be empty');
+        } elseif (strspn($addr, '.0123456789') == strlen($addr) ||
+                  strstr($addr, '/') !== false) {
+            $this->addr = $addr;
+        } else {
+            $this->addr = @gethostbyname($addr);
+        }
+
+        $this->port = $port % 65536;
+
+        if ($persistent !== null) {
+            $this->persistent = $persistent;
+        }
+
+        if ($timeout !== null) {
+            $this->timeout = $timeout;
+        }
+
+        $openfunc = $this->persistent ? 'pfsockopen' : 'fsockopen';
+        $errno = 0;
+        $errstr = '';
+        if ($options && function_exists('stream_context_create')) {
+            if ($this->timeout) {
+                $timeout = $this->timeout;
+            } else {
+                $timeout = 0;
+            }
+            $context = stream_context_create($options);
+            $fp = @$openfunc($this->addr, $this->port, $errno, $errstr, $timeout, $context);
+        } else {
+            if ($this->timeout) {
+                $fp = @$openfunc($this->addr, $this->port, $errno, $errstr, $this->timeout);
+            } else {
+                $fp = @$openfunc($this->addr, $this->port, $errno, $errstr);
+            }
+        }
+
+        if (!$fp) {
+            return $this->raiseError($errstr, $errno);
+        }
+
+        $this->fp = $fp;
+
+        return $this->setBlocking($this->blocking);
+    }
+
+    /**
+     * Disconnects from the peer, closes the socket.
+     *
+     * @access public
+     * @return mixed true on success or an error object otherwise
+     */
+    function disconnect()
+    {
+        if (!is_resource($this->fp)) {
+            return $this->raiseError('not connected');
+        }
+
+        @fclose($this->fp);
+        $this->fp = null;
+        return true;
+    }
+
+    /**
+     * Find out if the socket is in blocking mode.
+     *
+     * @access public
+     * @return boolean  The current blocking mode.
+     */
+    function isBlocking()
+    {
+        return $this->blocking;
+    }
+
+    /**
+     * Sets whether the socket connection should be blocking or
+     * not. A read call to a non-blocking socket will return immediately
+     * if there is no data available, whereas it will block until there
+     * is data for blocking sockets.
+     *
+     * @param boolean $mode  True for blocking sockets, false for nonblocking.
+     * @access public
+     * @return mixed true on success or an error object otherwise
+     */
+    function setBlocking($mode)
+    {
+        if (!is_resource($this->fp)) {
+            return $this->raiseError('not connected');
+        }
+
+        $this->blocking = $mode;
+        socket_set_blocking($this->fp, $this->blocking);
+        return true;
+    }
+
+    /**
+     * Sets the timeout value on socket descriptor,
+     * expressed in the sum of seconds and microseconds
+     *
+     * @param integer $seconds  Seconds.
+     * @param integer $microseconds  Microseconds.
+     * @access public
+     * @return mixed true on success or an error object otherwise
+     */
+    function setTimeout($seconds, $microseconds)
+    {
+        if (!is_resource($this->fp)) {
+            return $this->raiseError('not connected');
+        }
+
+        return socket_set_timeout($this->fp, $seconds, $microseconds);
+    }
+
+    /**
+     * Returns information about an existing socket resource.
+     * Currently returns four entries in the result array:
+     *
+     * <p>
+     * timed_out (bool) - The socket timed out waiting for data<br>
+     * blocked (bool) - The socket was blocked<br>
+     * eof (bool) - Indicates EOF event<br>
+     * unread_bytes (int) - Number of bytes left in the socket buffer<br>
+     * </p>
+     *
+     * @access public
+     * @return mixed Array containing information about existing socket resource or an error object otherwise
+     */
+    function getStatus()
+    {
+        if (!is_resource($this->fp)) {
+            return $this->raiseError('not connected');
+        }
+
+        return socket_get_status($this->fp);
+    }
+
+    /**
+     * Get a specified line of data
+     *
+     * @access public
+     * @return $size bytes of data from the socket, or a PEAR_Error if
+     *         not connected.
+     */
+    function gets($size)
+    {
+        if (!is_resource($this->fp)) {
+            return $this->raiseError('not connected');
+        }
+
+        return @fgets($this->fp, $size);
+    }
+
+    /**
+     * Read a specified amount of data. This is guaranteed to return,
+     * and has the added benefit of getting everything in one fread()
+     * chunk; if you know the size of the data you're getting
+     * beforehand, this is definitely the way to go.
+     *
+     * @param integer $size  The number of bytes to read from the socket.
+     * @access public
+     * @return $size bytes of data from the socket, or a PEAR_Error if
+     *         not connected.
+     */
+    function read($size)
+    {
+        if (!is_resource($this->fp)) {
+            return $this->raiseError('not connected');
+        }
+
+        return @fread($this->fp, $size);
+    }
+
+    /**
+     * Write a specified amount of data.
+     *
+     * @param string  $data       Data to write.
+     * @param integer $blocksize  Amount of data to write at once.
+     *                            NULL means all at once.
+     *
+     * @access public
+     * @return mixed true on success or an error object otherwise
+     */
+    function write($data, $blocksize = null)
+    {
+        if (!is_resource($this->fp)) {
+            return $this->raiseError('not connected');
+        }
+
+        if (is_null($blocksize) && !OS_WINDOWS) {
+            return fwrite($this->fp, $data);
+        } else {
+            if (is_null($blocksize)) {
+                $blocksize = 1024;
+            }
+
+            $pos = 0;
+            $size = strlen($data);
+            while ($pos < $size) {
+                $written = @fwrite($this->fp, substr($data, $pos, $blocksize));
+                if ($written === false) {
+                    return false;
+                }
+                $pos += $written;
+            }
+
+            return $pos;
+        }
+    }
+
+    /**
+     * Write a line of data to the socket, followed by a trailing "\r\n".
+     *
+     * @access public
+     * @return mixed fputs result, or an error
+     */
+    function writeLine($data)
+    {
+        if (!is_resource($this->fp)) {
+            return $this->raiseError('not connected');
+        }
+
+        return fwrite($this->fp, $data . "\r\n");
+    }
+
+    /**
+     * Tests for end-of-file on a socket descriptor.
+     *
+     * @access public
+     * @return bool
+     */
+    function eof()
+    {
+        return (is_resource($this->fp) && feof($this->fp));
+    }
+
+    /**
+     * Reads a byte of data
+     *
+     * @access public
+     * @return 1 byte of data from the socket, or a PEAR_Error if
+     *         not connected.
+     */
+    function readByte()
+    {
+        if (!is_resource($this->fp)) {
+            return $this->raiseError('not connected');
+        }
+
+        return ord(@fread($this->fp, 1));
+    }
+
+    /**
+     * Reads a word of data
+     *
+     * @access public
+     * @return 1 word of data from the socket, or a PEAR_Error if
+     *         not connected.
+     */
+    function readWord()
+    {
+        if (!is_resource($this->fp)) {
+            return $this->raiseError('not connected');
+        }
+
+        $buf = @fread($this->fp, 2);
+        return (ord($buf[0]) + (ord($buf[1]) << 8));
+    }
+
+    /**
+     * Reads an int of data
+     *
+     * @access public
+     * @return integer  1 int of data from the socket, or a PEAR_Error if
+     *                  not connected.
+     */
+    function readInt()
+    {
+        if (!is_resource($this->fp)) {
+            return $this->raiseError('not connected');
+        }
+
+        $buf = @fread($this->fp, 4);
+        return (ord($buf[0]) + (ord($buf[1]) << 8) +
+                (ord($buf[2]) << 16) + (ord($buf[3]) << 24));
+    }
+
+    /**
+     * Reads a zero-terminated string of data
+     *
+     * @access public
+     * @return string, or a PEAR_Error if
+     *         not connected.
+     */
+    function readString()
+    {
+        if (!is_resource($this->fp)) {
+            return $this->raiseError('not connected');
+        }
+
+        $string = '';
+        while (($char = @fread($this->fp, 1)) != "\x00")  {
+            $string .= $char;
+        }
+        return $string;
+    }
+
+    /**
+     * Reads an IP Address and returns it in a dot formated string
+     *
+     * @access public
+     * @return Dot formated string, or a PEAR_Error if
+     *         not connected.
+     */
+    function readIPAddress()
+    {
+        if (!is_resource($this->fp)) {
+            return $this->raiseError('not connected');
+        }
+
+        $buf = @fread($this->fp, 4);
+        return sprintf("%s.%s.%s.%s", ord($buf[0]), ord($buf[1]),
+                       ord($buf[2]), ord($buf[3]));
+    }
+
+    /**
+     * Read until either the end of the socket or a newline, whichever
+     * comes first. Strips the trailing newline from the returned data.
+     *
+     * @access public
+     * @return All available data up to a newline, without that
+     *         newline, or until the end of the socket, or a PEAR_Error if
+     *         not connected.
+     */
+    function readLine()
+    {
+        if (!is_resource($this->fp)) {
+            return $this->raiseError('not connected');
+        }
+
+        $line = '';
+        $timeout = time() + $this->timeout;
+        while (!feof($this->fp) && (!$this->timeout || time() < $timeout)) {
+            $line .= @fgets($this->fp, $this->lineLength);
+            if (substr($line, -1) == "\n") {
+                return rtrim($line, "\r\n");
+            }
+        }
+        return $line;
+    }
+
+    /**
+     * Read until the socket closes, or until there is no more data in
+     * the inner PHP buffer. If the inner buffer is empty, in blocking
+     * mode we wait for at least 1 byte of data. Therefore, in
+     * blocking mode, if there is no data at all to be read, this
+     * function will never exit (unless the socket is closed on the
+     * remote end).
+     *
+     * @access public
+     *
+     * @return string  All data until the socket closes, or a PEAR_Error if
+     *                 not connected.
+     */
+    function readAll()
+    {
+        if (!is_resource($this->fp)) {
+            return $this->raiseError('not connected');
+        }
+
+        $data = '';
+        while (!feof($this->fp)) {
+            $data .= @fread($this->fp, $this->lineLength);
+        }
+        return $data;
+    }
+
+    /**
+     * Runs the equivalent of the select() system call on the socket
+     * with a timeout specified by tv_sec and tv_usec.
+     *
+     * @param integer $state    Which of read/write/error to check for.
+     * @param integer $tv_sec   Number of seconds for timeout.
+     * @param integer $tv_usec  Number of microseconds for timeout.
+     *
+     * @access public
+     * @return False if select fails, integer describing which of read/write/error
+     *         are ready, or PEAR_Error if not connected.
+     */
+    function select($state, $tv_sec, $tv_usec = 0)
+    {
+        if (!is_resource($this->fp)) {
+            return $this->raiseError('not connected');
+        }
+
+        $read = null;
+        $write = null;
+        $except = null;
+        if ($state & NET_SOCKET_READ) {
+            $read[] = $this->fp;
+        }
+        if ($state & NET_SOCKET_WRITE) {
+            $write[] = $this->fp;
+        }
+        if ($state & NET_SOCKET_ERROR) {
+            $except[] = $this->fp;
+        }
+        if (false === ($sr = stream_select($read, $write, $except, $tv_sec, $tv_usec))) {
+            return false;
+        }
+
+        $result = 0;
+        if (count($read)) {
+            $result |= NET_SOCKET_READ;
+        }
+        if (count($write)) {
+            $result |= NET_SOCKET_WRITE;
+        }
+        if (count($except)) {
+            $result |= NET_SOCKET_ERROR;
+        }
+        return $result;
+    }
+
+}
diff --git a/lib/Net/URL.php b/lib/Net/URL.php
new file mode 100644 (file)
index 0000000..38e26fd
--- /dev/null
@@ -0,0 +1,410 @@
+<?php
+// +-----------------------------------------------------------------------+
+// | Copyright (c) 2002-2004, Richard Heyes                                |
+// | All rights reserved.                                                  |
+// |                                                                       |
+// | Redistribution and use in source and binary forms, with or without    |
+// | modification, are permitted provided that the following conditions    |
+// | are met:                                                              |
+// |                                                                       |
+// | o Redistributions of source code must retain the above copyright      |
+// |   notice, this list of conditions and the following disclaimer.       |
+// | o Redistributions in binary form must reproduce the above copyright   |
+// |   notice, this list of conditions and the following disclaimer in the |
+// |   documentation and/or other materials provided with the distribution.|
+// | o The names of the authors may not be used to endorse or promote      |
+// |   products derived from this software without specific prior written  |
+// |   permission.                                                         |
+// |                                                                       |
+// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS   |
+// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT     |
+// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
+// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT  |
+// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
+// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT      |
+// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
+// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
+// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT   |
+// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
+// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  |
+// |                                                                       |
+// +-----------------------------------------------------------------------+
+// | Author: Richard Heyes <richard at php net>                            |
+// +-----------------------------------------------------------------------+
+//
+// $Id: URL.php 32 2005-08-01 06:21:02Z dancoulter $
+//
+// Net_URL Class
+
+class Net_URL
+{
+    /**
+    * Full url
+    * @var string
+    */
+    var $url;
+
+    /**
+    * Protocol
+    * @var string
+    */
+    var $protocol;
+
+    /**
+    * Username
+    * @var string
+    */
+    var $username;
+
+    /**
+    * Password
+    * @var string
+    */
+    var $password;
+
+    /**
+    * Host
+    * @var string
+    */
+    var $host;
+
+    /**
+    * Port
+    * @var integer
+    */
+    var $port;
+
+    /**
+    * Path
+    * @var string
+    */
+    var $path;
+
+    /**
+    * Query string
+    * @var array
+    */
+    var $querystring;
+
+    /**
+    * Anchor
+    * @var string
+    */
+    var $anchor;
+
+    /**
+    * Whether to use []
+    * @var bool
+    */
+    var $useBrackets;
+
+    /**
+    * PHP4 Constructor
+    *
+    * @see __construct()
+    */
+    function Net_URL($url = null, $useBrackets = true)
+    {
+        $this->__construct($url, $useBrackets);
+    }
+
+    /**
+    * PHP5 Constructor
+    *
+    * Parses the given url and stores the various parts
+    * Defaults are used in certain cases
+    *
+    * @param string $url         Optional URL
+    * @param bool   $useBrackets Whether to use square brackets when
+    *                            multiple querystrings with the same name
+    *                            exist
+    */
+    function __construct($url = null, $useBrackets = true)
+    {
+        $HTTP_SERVER_VARS  = !empty($_SERVER) ? $_SERVER : $GLOBALS['HTTP_SERVER_VARS'];
+
+        $this->useBrackets = $useBrackets;
+        $this->url         = $url;
+        $this->user        = '';
+        $this->pass        = '';
+        $this->host        = '';
+        $this->port        = 80;
+        $this->path        = '';
+        $this->querystring = array();
+        $this->anchor      = '';
+
+        // Only use defaults if not an absolute URL given
+        if (!preg_match('/^[a-z0-9]+:\/\//i', $url)) {
+
+            $this->protocol    = (@$HTTP_SERVER_VARS['HTTPS'] == 'on' ? 'https' : 'http');
+
+            /**
+            * Figure out host/port
+            */
+            if (!empty($HTTP_SERVER_VARS['HTTP_HOST']) AND preg_match('/^(.*)(:([0-9]+))?$/U', $HTTP_SERVER_VARS['HTTP_HOST'], $matches)) {
+                $host = $matches[1];
+                if (!empty($matches[3])) {
+                    $port = $matches[3];
+                } else {
+                    $port = $this->getStandardPort($this->protocol);
+                }
+            }
+
+            $this->user        = '';
+            $this->pass        = '';
+            $this->host        = !empty($host) ? $host : (isset($HTTP_SERVER_VARS['SERVER_NAME']) ? $HTTP_SERVER_VARS['SERVER_NAME'] : 'localhost');
+            $this->port        = !empty($port) ? $port : (isset($HTTP_SERVER_VARS['SERVER_PORT']) ? $HTTP_SERVER_VARS['SERVER_PORT'] : $this->getStandardPort($this->protocol));
+            $this->path        = !empty($HTTP_SERVER_VARS['PHP_SELF']) ? $HTTP_SERVER_VARS['PHP_SELF'] : '/';
+            $this->querystring = isset($HTTP_SERVER_VARS['QUERY_STRING']) ? $this->_parseRawQuerystring($HTTP_SERVER_VARS['QUERY_STRING']) : null;
+            $this->anchor      = '';
+        }
+
+        // Parse the url and store the various parts
+        if (!empty($url)) {
+            $urlinfo = parse_url($url);
+
+            // Default querystring
+            $this->querystring = array();
+
+            foreach ($urlinfo as $key => $value) {
+                switch ($key) {
+                    case 'scheme':
+                        $this->protocol = $value;
+                        $this->port     = $this->getStandardPort($value);
+                        break;
+
+                    case 'user':
+                    case 'pass':
+                    case 'host':
+                    case 'port':
+                        $this->$key = $value;
+                        break;
+
+                    case 'path':
+                        if ($value{0} == '/') {
+                            $this->path = $value;
+                        } else {
+                            $path = dirname($this->path) == DIRECTORY_SEPARATOR ? '' : dirname($this->path);
+                            $this->path = sprintf('%s/%s', $path, $value);
+                        }
+                        break;
+
+                    case 'query':
+                        $this->querystring = $this->_parseRawQueryString($value);
+                        break;
+
+                    case 'fragment':
+                        $this->anchor = $value;
+                        break;
+                }
+            }
+        }
+    }
+
+    /**
+    * Returns full url
+    *
+    * @return string Full url
+    * @access public
+    */
+    function getURL()
+    {
+        $querystring = $this->getQueryString();
+
+        $this->url = $this->protocol . '://'
+                   . $this->user . (!empty($this->pass) ? ':' : '')
+                   . $this->pass . (!empty($this->user) ? '@' : '')
+                   . $this->host . ($this->port == $this->getStandardPort($this->protocol) ? '' : ':' . $this->port)
+                   . $this->path
+                   . (!empty($querystring) ? '?' . $querystring : '')
+                   . (!empty($this->anchor) ? '#' . $this->anchor : '');
+
+        return $this->url;
+    }
+
+    /**
+    * Adds a querystring item
+    *
+    * @param  string $name       Name of item
+    * @param  string $value      Value of item
+    * @param  bool   $preencoded Whether value is urlencoded or not, default = not
+    * @access public
+    */
+    function addQueryString($name, $value, $preencoded = false)
+    {
+        if ($preencoded) {
+            $this->querystring[$name] = $value;
+        } else {
+            $this->querystring[$name] = is_array($value) ? array_map('rawurlencode', $value): rawurlencode($value);
+        }
+    }
+
+    /**
+    * Removes a querystring item
+    *
+    * @param  string $name Name of item
+    * @access public
+    */
+    function removeQueryString($name)
+    {
+        if (isset($this->querystring[$name])) {
+            unset($this->querystring[$name]);
+        }
+    }
+
+    /**
+    * Sets the querystring to literally what you supply
+    *
+    * @param  string $querystring The querystring data. Should be of the format foo=bar&x=y etc
+    * @access public
+    */
+    function addRawQueryString($querystring)
+    {
+        $this->querystring = $this->_parseRawQueryString($querystring);
+    }
+
+    /**
+    * Returns flat querystring
+    *
+    * @return string Querystring
+    * @access public
+    */
+    function getQueryString()
+    {
+        if (!empty($this->querystring)) {
+            foreach ($this->querystring as $name => $value) {
+                if (is_array($value)) {
+                    foreach ($value as $k => $v) {
+                        $querystring[] = $this->useBrackets ? sprintf('%s[%s]=%s', $name, $k, $v) : ($name . '=' . $v);
+                    }
+                } elseif (!is_null($value)) {
+                    $querystring[] = $name . '=' . $value;
+                } else {
+                    $querystring[] = $name;
+                }
+            }
+            $querystring = implode(ini_get('arg_separator.output'), $querystring);
+        } else {
+            $querystring = '';
+        }
+
+        return $querystring;
+    }
+
+    /**
+    * Parses raw querystring and returns an array of it
+    *
+    * @param  string  $querystring The querystring to parse
+    * @return array                An array of the querystring data
+    * @access private
+    */
+    function _parseRawQuerystring($querystring)
+    {
+        $parts  = preg_split('/[' . preg_quote(ini_get('arg_separator.input'), '/') . ']/', $querystring, -1, PREG_SPLIT_NO_EMPTY);
+        $return = array();
+
+        foreach ($parts as $part) {
+            if (strpos($part, '=') !== false) {
+                $value = substr($part, strpos($part, '=') + 1);
+                $key   = substr($part, 0, strpos($part, '='));
+            } else {
+                $value = null;
+                $key   = $part;
+            }
+            if (substr($key, -2) == '[]') {
+                $key = substr($key, 0, -2);
+                if (@!is_array($return[$key])) {
+                    $return[$key]   = array();
+                    $return[$key][] = $value;
+                } else {
+                    $return[$key][] = $value;
+                }
+            } elseif (!$this->useBrackets AND !empty($return[$key])) {
+                $return[$key]   = (array)$return[$key];
+                $return[$key][] = $value;
+            } else {
+                $return[$key] = $value;
+            }
+        }
+
+        return $return;
+    }
+
+    /**
+    * Resolves //, ../ and ./ from a path and returns
+    * the result. Eg:
+    *
+    * /foo/bar/../boo.php    => /foo/boo.php
+    * /foo/bar/../../boo.php => /boo.php
+    * /foo/bar/.././/boo.php => /foo/boo.php
+    *
+    * This method can also be called statically.
+    *
+    * @param  string $url URL path to resolve
+    * @return string      The result
+    */
+    function resolvePath($path)
+    {
+        $path = explode('/', str_replace('//', '/', $path));
+
+        for ($i=0; $i<count($path); $i++) {
+            if ($path[$i] == '.') {
+                unset($path[$i]);
+                $path = array_values($path);
+                $i--;
+
+            } elseif ($path[$i] == '..' AND ($i > 1 OR ($i == 1 AND $path[0] != '') ) ) {
+                unset($path[$i]);
+                unset($path[$i-1]);
+                $path = array_values($path);
+                $i -= 2;
+
+            } elseif ($path[$i] == '..' AND $i == 1 AND $path[0] == '') {
+                unset($path[$i]);
+                $path = array_values($path);
+                $i--;
+
+            } else {
+                continue;
+            }
+        }
+
+        return implode('/', $path);
+    }
+
+    /**
+    * Returns the standard port number for a protocol
+    *
+    * @param  string  $scheme The protocol to lookup
+    * @return integer         Port number or NULL if no scheme matches
+    *
+    * @author Philippe Jausions <Philippe.Jausions@11abacus.com>
+    */
+    function getStandardPort($scheme)
+    {
+        switch (strtolower($scheme)) {
+            case 'http':    return 80;
+            case 'https':   return 443;
+            case 'ftp':     return 21;
+            case 'imap':    return 143;
+            case 'imaps':   return 993;
+            case 'pop3':    return 110;
+            case 'pop3s':   return 995;
+            default:        return null;
+       }
+    }
+
+    /**
+    * Forces the URL to a particular protocol
+    *
+    * @param string  $protocol Protocol to force the URL to
+    * @param integer $port     Optional port (standard port is used by default)
+    */
+    function setProtocol($protocol, $port = null)
+    {
+        $this->protocol = $protocol;
+        $this->port = is_null($port) ? $this->getStandardPort() : $port;
+    }
+
+}
+?>
diff --git a/lib/PEAR.php b/lib/PEAR.php
new file mode 100644 (file)
index 0000000..e442d8c
--- /dev/null
@@ -0,0 +1,1108 @@
+<?php
+/**
+ * PEAR, the PHP Extension and Application Repository
+ *
+ * PEAR class and PEAR_Error class
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   pear
+ * @package    PEAR
+ * @author     Sterling Hughes <sterling@php.net>
+ * @author     Stig Bakken <ssb@php.net>
+ * @author     Tomas V.V.Cox <cox@idecnet.com>
+ * @author     Greg Beaver <cellog@php.net>
+ * @copyright  1997-2006 The PHP Group
+ * @license    http://www.php.net/license/3_0.txt  PHP License 3.0
+ * @version    CVS: $Id: PEAR.php 127 2008-01-17 20:21:37Z dcoulter $
+ * @link       http://pear.php.net/package/PEAR
+ * @since      File available since Release 0.1
+ */
+
+/**#@+
+ * ERROR constants
+ */
+define('PEAR_ERROR_RETURN',     1);
+define('PEAR_ERROR_PRINT',      2);
+define('PEAR_ERROR_TRIGGER',    4);
+define('PEAR_ERROR_DIE',        8);
+define('PEAR_ERROR_CALLBACK',  16);
+/**
+ * WARNING: obsolete
+ * @deprecated
+ */
+define('PEAR_ERROR_EXCEPTION', 32);
+/**#@-*/
+define('PEAR_ZE2', (function_exists('version_compare') &&
+                    version_compare(zend_version(), "2-dev", "ge")));
+
+if (substr(PHP_OS, 0, 3) == 'WIN') {
+    define('OS_WINDOWS', true);
+    define('OS_UNIX',    false);
+    define('PEAR_OS',    'Windows');
+} else {
+    define('OS_WINDOWS', false);
+    define('OS_UNIX',    true);
+    define('PEAR_OS',    'Unix'); // blatant assumption
+}
+
+// instant backwards compatibility
+if (!defined('PATH_SEPARATOR')) {
+    if (OS_WINDOWS) {
+        define('PATH_SEPARATOR', ';');
+    } else {
+        define('PATH_SEPARATOR', ':');
+    }
+}
+
+$GLOBALS['_PEAR_default_error_mode']     = PEAR_ERROR_RETURN;
+$GLOBALS['_PEAR_default_error_options']  = E_USER_NOTICE;
+$GLOBALS['_PEAR_destructor_object_list'] = array();
+$GLOBALS['_PEAR_shutdown_funcs']         = array();
+$GLOBALS['_PEAR_error_handler_stack']    = array();
+
+@ini_set('track_errors', true);
+
+/**
+ * Base class for other PEAR classes.  Provides rudimentary
+ * emulation of destructors.
+ *
+ * If you want a destructor in your class, inherit PEAR and make a
+ * destructor method called _yourclassname (same name as the
+ * constructor, but with a "_" prefix).  Also, in your constructor you
+ * have to call the PEAR constructor: $this->PEAR();.
+ * The destructor method will be called without parameters.  Note that
+ * at in some SAPI implementations (such as Apache), any output during
+ * the request shutdown (in which destructors are called) seems to be
+ * discarded.  If you need to get any debug information from your
+ * destructor, use error_log(), syslog() or something similar.
+ *
+ * IMPORTANT! To use the emulated destructors you need to create the
+ * objects by reference: $obj =& new PEAR_child;
+ *
+ * @category   pear
+ * @package    PEAR
+ * @author     Stig Bakken <ssb@php.net>
+ * @author     Tomas V.V. Cox <cox@idecnet.com>
+ * @author     Greg Beaver <cellog@php.net>
+ * @copyright  1997-2006 The PHP Group
+ * @license    http://www.php.net/license/3_0.txt  PHP License 3.0
+ * @version    Release: 1.6.2
+ * @link       http://pear.php.net/package/PEAR
+ * @see        PEAR_Error
+ * @since      Class available since PHP 4.0.2
+ * @link        http://pear.php.net/manual/en/core.pear.php#core.pear.pear
+ */
+class PEAR
+{
+    // {{{ properties
+
+    /**
+     * Whether to enable internal debug messages.
+     *
+     * @var     bool
+     * @access  private
+     */
+    var $_debug = false;
+
+    /**
+     * Default error mode for this object.
+     *
+     * @var     int
+     * @access  private
+     */
+    var $_default_error_mode = null;
+
+    /**
+     * Default error options used for this object when error mode
+     * is PEAR_ERROR_TRIGGER.
+     *
+     * @var     int
+     * @access  private
+     */
+    var $_default_error_options = null;
+
+    /**
+     * Default error handler (callback) for this object, if error mode is
+     * PEAR_ERROR_CALLBACK.
+     *
+     * @var     string
+     * @access  private
+     */
+    var $_default_error_handler = '';
+
+    /**
+     * Which class to use for error objects.
+     *
+     * @var     string
+     * @access  private
+     */
+    var $_error_class = 'PEAR_Error';
+
+    /**
+     * An array of expected errors.
+     *
+     * @var     array
+     * @access  private
+     */
+    var $_expected_errors = array();
+
+    // }}}
+
+    // {{{ constructor
+
+    /**
+     * Constructor.  Registers this object in
+     * $_PEAR_destructor_object_list for destructor emulation if a
+     * destructor object exists.
+     *
+     * @param string $error_class  (optional) which class to use for
+     *        error objects, defaults to PEAR_Error.
+     * @access public
+     * @return void
+     */
+    function PEAR($error_class = null)
+    {
+        $classname = strtolower(get_class($this));
+        if ($this->_debug) {
+            print "PEAR constructor called, class=$classname\n";
+        }
+        if ($error_class !== null) {
+            $this->_error_class = $error_class;
+        }
+        while ($classname && strcasecmp($classname, "pear")) {
+            $destructor = "_$classname";
+            if (method_exists($this, $destructor)) {
+                global $_PEAR_destructor_object_list;
+                $_PEAR_destructor_object_list[] = &$this;
+                if (!isset($GLOBALS['_PEAR_SHUTDOWN_REGISTERED'])) {
+                    register_shutdown_function("_PEAR_call_destructors");
+                    $GLOBALS['_PEAR_SHUTDOWN_REGISTERED'] = true;
+                }
+                break;
+            } else {
+                $classname = get_parent_class($classname);
+            }
+        }
+    }
+
+    // }}}
+    // {{{ destructor
+
+    /**
+     * Destructor (the emulated type of...).  Does nothing right now,
+     * but is included for forward compatibility, so subclass
+     * destructors should always call it.
+     *
+     * See the note in the class desciption about output from
+     * destructors.
+     *
+     * @access public
+     * @return void
+     */
+    function _PEAR() {
+        if ($this->_debug) {
+            printf("PEAR destructor called, class=%s\n", strtolower(get_class($this)));
+        }
+    }
+
+    // }}}
+    // {{{ getStaticProperty()
+
+    /**
+    * If you have a class that's mostly/entirely static, and you need static
+    * properties, you can use this method to simulate them. Eg. in your method(s)
+    * do this: $myVar = &PEAR::getStaticProperty('myclass', 'myVar');
+    * You MUST use a reference, or they will not persist!
+    *
+    * @access public
+    * @param  string $class  The calling classname, to prevent clashes
+    * @param  string $var    The variable to retrieve.
+    * @return mixed   A reference to the variable. If not set it will be
+    *                 auto initialised to NULL.
+    */
+    function &getStaticProperty($class, $var)
+    {
+        static $properties;
+        if (!isset($properties[$class])) {
+            $properties[$class] = array();
+        }
+        if (!array_key_exists($var, $properties[$class])) {
+            $properties[$class][$var] = null;
+        }
+        return $properties[$class][$var];
+    }
+
+    // }}}
+    // {{{ registerShutdownFunc()
+
+    /**
+    * Use this function to register a shutdown method for static
+    * classes.
+    *
+    * @access public
+    * @param  mixed $func  The function name (or array of class/method) to call
+    * @param  mixed $args  The arguments to pass to the function
+    * @return void
+    */
+    function registerShutdownFunc($func, $args = array())
+    {
+        // if we are called statically, there is a potential
+        // that no shutdown func is registered.  Bug #6445
+        if (!isset($GLOBALS['_PEAR_SHUTDOWN_REGISTERED'])) {
+            register_shutdown_function("_PEAR_call_destructors");
+            $GLOBALS['_PEAR_SHUTDOWN_REGISTERED'] = true;
+        }
+        $GLOBALS['_PEAR_shutdown_funcs'][] = array($func, $args);
+    }
+
+    // }}}
+    // {{{ isError()
+
+    /**
+     * Tell whether a value is a PEAR error.
+     *
+     * @param   mixed $data   the value to test
+     * @param   int   $code   if $data is an error object, return true
+     *                        only if $code is a string and
+     *                        $obj->getMessage() == $code or
+     *                        $code is an integer and $obj->getCode() == $code
+     * @access  public
+     * @return  bool    true if parameter is an error
+     */
+    function isError($data, $code = null)
+    {
+        if (is_a($data, 'PEAR_Error')) {
+            if (is_null($code)) {
+                return true;
+            } elseif (is_string($code)) {
+                return $data->getMessage() == $code;
+            } else {
+                return $data->getCode() == $code;
+            }
+        }
+        return false;
+    }
+
+    // }}}
+    // {{{ setErrorHandling()
+
+    /**
+     * Sets how errors generated by this object should be handled.
+     * Can be invoked both in objects and statically.  If called
+     * statically, setErrorHandling sets the default behaviour for all
+     * PEAR objects.  If called in an object, setErrorHandling sets
+     * the default behaviour for that object.
+     *
+     * @param int $mode
+     *        One of PEAR_ERROR_RETURN, PEAR_ERROR_PRINT,
+     *        PEAR_ERROR_TRIGGER, PEAR_ERROR_DIE,
+     *        PEAR_ERROR_CALLBACK or PEAR_ERROR_EXCEPTION.
+     *
+     * @param mixed $options
+     *        When $mode is PEAR_ERROR_TRIGGER, this is the error level (one
+     *        of E_USER_NOTICE, E_USER_WARNING or E_USER_ERROR).
+     *
+     *        When $mode is PEAR_ERROR_CALLBACK, this parameter is expected
+     *        to be the callback function or method.  A callback
+     *        function is a string with the name of the function, a
+     *        callback method is an array of two elements: the element
+     *        at index 0 is the object, and the element at index 1 is
+     *        the name of the method to call in the object.
+     *
+     *        When $mode is PEAR_ERROR_PRINT or PEAR_ERROR_DIE, this is
+     *        a printf format string used when printing the error
+     *        message.
+     *
+     * @access public
+     * @return void
+     * @see PEAR_ERROR_RETURN
+     * @see PEAR_ERROR_PRINT
+     * @see PEAR_ERROR_TRIGGER
+     * @see PEAR_ERROR_DIE
+     * @see PEAR_ERROR_CALLBACK
+     * @see PEAR_ERROR_EXCEPTION
+     *
+     * @since PHP 4.0.5
+     */
+
+    function setErrorHandling($mode = null, $options = null)
+    {
+        if (isset($this) && is_a($this, 'PEAR')) {
+            $setmode     = &$this->_default_error_mode;
+            $setoptions  = &$this->_default_error_options;
+        } else {
+            $setmode     = &$GLOBALS['_PEAR_default_error_mode'];
+            $setoptions  = &$GLOBALS['_PEAR_default_error_options'];
+        }
+
+        switch ($mode) {
+            case PEAR_ERROR_EXCEPTION:
+            case PEAR_ERROR_RETURN:
+            case PEAR_ERROR_PRINT:
+            case PEAR_ERROR_TRIGGER:
+            case PEAR_ERROR_DIE:
+            case null:
+                $setmode = $mode;
+                $setoptions = $options;
+                break;
+
+            case PEAR_ERROR_CALLBACK:
+                $setmode = $mode;
+                // class/object method callback
+                if (is_callable($options)) {
+                    $setoptions = $options;
+                } else {
+                    trigger_error("invalid error callback", E_USER_WARNING);
+                }
+                break;
+
+            default:
+                trigger_error("invalid error mode", E_USER_WARNING);
+                break;
+        }
+    }
+
+    // }}}
+    // {{{ expectError()
+
+    /**
+     * This method is used to tell which errors you expect to get.
+     * Expected errors are always returned with error mode
+     * PEAR_ERROR_RETURN.  Expected error codes are stored in a stack,
+     * and this method pushes a new element onto it.  The list of
+     * expected errors are in effect until they are popped off the
+     * stack with the popExpect() method.
+     *
+     * Note that this method can not be called statically
+     *
+     * @param mixed $code a single error code or an array of error codes to expect
+     *
+     * @return int     the new depth of the "expected errors" stack
+     * @access public
+     */
+    function expectError($code = '*')
+    {
+        if (is_array($code)) {
+            array_push($this->_expected_errors, $code);
+        } else {
+            array_push($this->_expected_errors, array($code));
+        }
+        return sizeof($this->_expected_errors);
+    }
+
+    // }}}
+    // {{{ popExpect()
+
+    /**
+     * This method pops one element off the expected error codes
+     * stack.
+     *
+     * @return array   the list of error codes that were popped
+     */
+    function popExpect()
+    {
+        return array_pop($this->_expected_errors);
+    }
+
+    // }}}
+    // {{{ _checkDelExpect()
+
+    /**
+     * This method checks unsets an error code if available
+     *
+     * @param mixed error code
+     * @return bool true if the error code was unset, false otherwise
+     * @access private
+     * @since PHP 4.3.0
+     */
+    function _checkDelExpect($error_code)
+    {
+        $deleted = false;
+
+        foreach ($this->_expected_errors AS $key => $error_array) {
+            if (in_array($error_code, $error_array)) {
+                unset($this->_expected_errors[$key][array_search($error_code, $error_array)]);
+                $deleted = true;
+            }
+
+            // clean up empty arrays
+            if (0 == count($this->_expected_errors[$key])) {
+                unset($this->_expected_errors[$key]);
+            }
+        }
+        return $deleted;
+    }
+
+    // }}}
+    // {{{ delExpect()
+
+    /**
+     * This method deletes all occurences of the specified element from
+     * the expected error codes stack.
+     *
+     * @param  mixed $error_code error code that should be deleted
+     * @return mixed list of error codes that were deleted or error
+     * @access public
+     * @since PHP 4.3.0
+     */
+    function delExpect($error_code)
+    {
+        $deleted = false;
+
+        if ((is_array($error_code) && (0 != count($error_code)))) {
+            // $error_code is a non-empty array here;
+            // we walk through it trying to unset all
+            // values
+            foreach($error_code as $key => $error) {
+                if ($this->_checkDelExpect($error)) {
+                    $deleted =  true;
+                } else {
+                    $deleted = false;
+                }
+            }
+            return $deleted ? true : PEAR::raiseError("The expected error you submitted does not exist"); // IMPROVE ME
+        } elseif (!empty($error_code)) {
+            // $error_code comes alone, trying to unset it
+            if ($this->_checkDelExpect($error_code)) {
+                return true;
+            } else {
+                return PEAR::raiseError("The expected error you submitted does not exist"); // IMPROVE ME
+            }
+        } else {
+            // $error_code is empty
+            return PEAR::raiseError("The expected error you submitted is empty"); // IMPROVE ME
+        }
+    }
+
+    // }}}
+    // {{{ raiseError()
+
+    /**
+     * This method is a wrapper that returns an instance of the
+     * configured error class with this object's default error
+     * handling applied.  If the $mode and $options parameters are not
+     * specified, the object's defaults are used.
+     *
+     * @param mixed $message a text error message or a PEAR error object
+     *
+     * @param int $code      a numeric error code (it is up to your class
+     *                  to define these if you want to use codes)
+     *
+     * @param int $mode      One of PEAR_ERROR_RETURN, PEAR_ERROR_PRINT,
+     *                  PEAR_ERROR_TRIGGER, PEAR_ERROR_DIE,
+     *                  PEAR_ERROR_CALLBACK, PEAR_ERROR_EXCEPTION.
+     *
+     * @param mixed $options If $mode is PEAR_ERROR_TRIGGER, this parameter
+     *                  specifies the PHP-internal error level (one of
+     *                  E_USER_NOTICE, E_USER_WARNING or E_USER_ERROR).
+     *                  If $mode is PEAR_ERROR_CALLBACK, this
+     *                  parameter specifies the callback function or
+     *                  method.  In other error modes this parameter
+     *                  is ignored.
+     *
+     * @param string $userinfo If you need to pass along for example debug
+     *                  information, this parameter is meant for that.
+     *
+     * @param string $error_class The returned error object will be
+     *                  instantiated from this class, if specified.
+     *
+     * @param bool $skipmsg If true, raiseError will only pass error codes,
+     *                  the error message parameter will be dropped.
+     *
+     * @access public
+     * @return object   a PEAR error object
+     * @see PEAR::setErrorHandling
+     * @since PHP 4.0.5
+     */
+    function &raiseError($message = null,
+                         $code = null,
+                         $mode = null,
+                         $options = null,
+                         $userinfo = null,
+                         $error_class = null,
+                         $skipmsg = false)
+    {
+        // The error is yet a PEAR error object
+        if (is_object($message)) {
+            $code        = $message->getCode();
+            $userinfo    = $message->getUserInfo();
+            $error_class = $message->getType();
+            $message->error_message_prefix = '';
+            $message     = $message->getMessage();
+        }
+
+        if (isset($this) && isset($this->_expected_errors) && sizeof($this->_expected_errors) > 0 && sizeof($exp = end($this->_expected_errors))) {
+            if ($exp[0] == "*" ||
+                (is_int(reset($exp)) && in_array($code, $exp)) ||
+                (is_string(reset($exp)) && in_array($message, $exp))) {
+                $mode = PEAR_ERROR_RETURN;
+            }
+        }
+        // No mode given, try global ones
+        if ($mode === null) {
+            // Class error handler
+            if (isset($this) && isset($this->_default_error_mode)) {
+                $mode    = $this->_default_error_mode;
+                $options = $this->_default_error_options;
+            // Global error handler
+            } elseif (isset($GLOBALS['_PEAR_default_error_mode'])) {
+                $mode    = $GLOBALS['_PEAR_default_error_mode'];
+                $options = $GLOBALS['_PEAR_default_error_options'];
+            }
+        }
+
+        if ($error_class !== null) {
+            $ec = $error_class;
+        } elseif (isset($this) && isset($this->_error_class)) {
+            $ec = $this->_error_class;
+        } else {
+            $ec = 'PEAR_Error';
+        }
+        if ($skipmsg) {
+            $a = &new $ec($code, $mode, $options, $userinfo);
+            return $a;
+        } else {
+            $a = &new $ec($message, $code, $mode, $options, $userinfo);
+            return $a;
+        }
+    }
+
+    // }}}
+    // {{{ throwError()
+
+    /**
+     * Simpler form of raiseError with fewer options.  In most cases
+     * message, code and userinfo are enough.
+     *
+     * @param string $message
+     *
+     */
+    function &throwError($message = null,
+                         $code = null,
+                         $userinfo = null)
+    {
+        if (isset($this) && is_a($this, 'PEAR')) {
+            $a = &$this->raiseError($message, $code, null, null, $userinfo);
+            return $a;
+        } else {
+            $a = &PEAR::raiseError($message, $code, null, null, $userinfo);
+            return $a;
+        }
+    }
+
+    // }}}
+    function staticPushErrorHandling($mode, $options = null)
+    {
+        $stack = &$GLOBALS['_PEAR_error_handler_stack'];
+        $def_mode    = &$GLOBALS['_PEAR_default_error_mode'];
+        $def_options = &$GLOBALS['_PEAR_default_error_options'];
+        $stack[] = array($def_mode, $def_options);
+        switch ($mode) {
+            case PEAR_ERROR_EXCEPTION:
+            case PEAR_ERROR_RETURN:
+            case PEAR_ERROR_PRINT:
+            case PEAR_ERROR_TRIGGER:
+            case PEAR_ERROR_DIE:
+            case null:
+                $def_mode = $mode;
+                $def_options = $options;
+                break;
+
+            case PEAR_ERROR_CALLBACK:
+                $def_mode = $mode;
+                // class/object method callback
+                if (is_callable($options)) {
+                    $def_options = $options;
+                } else {
+                    trigger_error("invalid error callback", E_USER_WARNING);
+                }
+                break;
+
+            default:
+                trigger_error("invalid error mode", E_USER_WARNING);
+                break;
+        }
+        $stack[] = array($mode, $options);
+        return true;
+    }
+
+    function staticPopErrorHandling()
+    {
+        $stack = &$GLOBALS['_PEAR_error_handler_stack'];
+        $setmode     = &$GLOBALS['_PEAR_default_error_mode'];
+        $setoptions  = &$GLOBALS['_PEAR_default_error_options'];
+        array_pop($stack);
+        list($mode, $options) = $stack[sizeof($stack) - 1];
+        array_pop($stack);
+        switch ($mode) {
+            case PEAR_ERROR_EXCEPTION:
+            case PEAR_ERROR_RETURN:
+            case PEAR_ERROR_PRINT:
+            case PEAR_ERROR_TRIGGER:
+            case PEAR_ERROR_DIE:
+            case null:
+                $setmode = $mode;
+                $setoptions = $options;
+                break;
+
+            case PEAR_ERROR_CALLBACK:
+                $setmode = $mode;
+                // class/object method callback
+                if (is_callable($options)) {
+                    $setoptions = $options;
+                } else {
+                    trigger_error("invalid error callback", E_USER_WARNING);
+                }
+                break;
+
+            default:
+                trigger_error("invalid error mode", E_USER_WARNING);
+                break;
+        }
+        return true;
+    }
+
+    // {{{ pushErrorHandling()
+
+    /**
+     * Push a new error handler on top of the error handler options stack. With this
+     * you can easily override the actual error handler for some code and restore
+     * it later with popErrorHandling.
+     *
+     * @param mixed $mode (same as setErrorHandling)
+     * @param mixed $options (same as setErrorHandling)
+     *
+     * @return bool Always true
+     *
+     * @see PEAR::setErrorHandling
+     */
+    function pushErrorHandling($mode, $options = null)
+    {
+        $stack = &$GLOBALS['_PEAR_error_handler_stack'];
+        if (isset($this) && is_a($this, 'PEAR')) {
+            $def_mode    = &$this->_default_error_mode;
+            $def_options = &$this->_default_error_options;
+        } else {
+            $def_mode    = &$GLOBALS['_PEAR_default_error_mode'];
+            $def_options = &$GLOBALS['_PEAR_default_error_options'];
+        }
+        $stack[] = array($def_mode, $def_options);
+
+        if (isset($this) && is_a($this, 'PEAR')) {
+            $this->setErrorHandling($mode, $options);
+        } else {
+            PEAR::setErrorHandling($mode, $options);
+        }
+        $stack[] = array($mode, $options);
+        return true;
+    }
+
+    // }}}
+    // {{{ popErrorHandling()
+
+    /**
+    * Pop the last error handler used
+    *
+    * @return bool Always true
+    *
+    * @see PEAR::pushErrorHandling
+    */
+    function popErrorHandling()
+    {
+        $stack = &$GLOBALS['_PEAR_error_handler_stack'];
+        array_pop($stack);
+        list($mode, $options) = $stack[sizeof($stack) - 1];
+        array_pop($stack);
+        if (isset($this) && is_a($this, 'PEAR')) {
+            $this->setErrorHandling($mode, $options);
+        } else {
+            PEAR::setErrorHandling($mode, $options);
+        }
+        return true;
+    }
+
+    // }}}
+    // {{{ loadExtension()
+
+    /**
+    * OS independant PHP extension load. Remember to take care
+    * on the correct extension name for case sensitive OSes.
+    *
+    * @param string $ext The extension name
+    * @return bool Success or not on the dl() call
+    */
+    function loadExtension($ext)
+    {
+        if (!extension_loaded($ext)) {
+            // if either returns true dl() will produce a FATAL error, stop that
+            if ((ini_get('enable_dl') != 1) || (ini_get('safe_mode') == 1)) {
+                return false;
+            }
+            if (OS_WINDOWS) {
+                $suffix = '.dll';
+            } elseif (PHP_OS == 'HP-UX') {
+                $suffix = '.sl';
+            } elseif (PHP_OS == 'AIX') {
+                $suffix = '.a';
+            } elseif (PHP_OS == 'OSX') {
+                $suffix = '.bundle';
+            } else {
+                $suffix = '.so';
+            }
+            return @dl('php_'.$ext.$suffix) || @dl($ext.$suffix);
+        }
+        return true;
+    }
+
+    // }}}
+}
+
+// {{{ _PEAR_call_destructors()
+
+function _PEAR_call_destructors()
+{
+    global $_PEAR_destructor_object_list;
+    if (is_array($_PEAR_destructor_object_list) &&
+        sizeof($_PEAR_destructor_object_list))
+    {
+        reset($_PEAR_destructor_object_list);
+        if (PEAR::getStaticProperty('PEAR', 'destructlifo')) {
+            $_PEAR_destructor_object_list = array_reverse($_PEAR_destructor_object_list);
+        }
+        while (list($k, $objref) = each($_PEAR_destructor_object_list)) {
+            $classname = get_class($objref);
+            while ($classname) {
+                $destructor = "_$classname";
+                if (method_exists($objref, $destructor)) {
+                    $objref->$destructor();
+                    break;
+                } else {
+                    $classname = get_parent_class($classname);
+                }
+            }
+        }
+        // Empty the object list to ensure that destructors are
+        // not called more than once.
+        $_PEAR_destructor_object_list = array();
+    }
+
+    // Now call the shutdown functions
+    if (is_array($GLOBALS['_PEAR_shutdown_funcs']) AND !empty($GLOBALS['_PEAR_shutdown_funcs'])) {
+        foreach ($GLOBALS['_PEAR_shutdown_funcs'] as $value) {
+            call_user_func_array($value[0], $value[1]);
+        }
+    }
+}
+
+// }}}
+/**
+ * Standard PEAR error class for PHP 4
+ *
+ * This class is supserseded by {@link PEAR_Exception} in PHP 5
+ *
+ * @category   pear
+ * @package    PEAR
+ * @author     Stig Bakken <ssb@php.net>
+ * @author     Tomas V.V. Cox <cox@idecnet.com>
+ * @author     Gregory Beaver <cellog@php.net>
+ * @copyright  1997-2006 The PHP Group
+ * @license    http://www.php.net/license/3_0.txt  PHP License 3.0
+ * @version    Release: 1.6.2
+ * @link       http://pear.php.net/manual/en/core.pear.pear-error.php
+ * @see        PEAR::raiseError(), PEAR::throwError()
+ * @since      Class available since PHP 4.0.2
+ */
+class PEAR_Error
+{
+    // {{{ properties
+
+    var $error_message_prefix = '';
+    var $mode                 = PEAR_ERROR_RETURN;
+    var $level                = E_USER_NOTICE;
+    var $code                 = -1;
+    var $message              = '';
+    var $userinfo             = '';
+    var $backtrace            = null;
+
+    // }}}
+    // {{{ constructor
+
+    /**
+     * PEAR_Error constructor
+     *
+     * @param string $message  message
+     *
+     * @param int $code     (optional) error code
+     *
+     * @param int $mode     (optional) error mode, one of: PEAR_ERROR_RETURN,
+     * PEAR_ERROR_PRINT, PEAR_ERROR_DIE, PEAR_ERROR_TRIGGER,
+     * PEAR_ERROR_CALLBACK or PEAR_ERROR_EXCEPTION
+     *
+     * @param mixed $options   (optional) error level, _OR_ in the case of
+     * PEAR_ERROR_CALLBACK, the callback function or object/method
+     * tuple.
+     *
+     * @param string $userinfo (optional) additional user/debug info
+     *
+     * @access public
+     *
+     */
+    function PEAR_Error($message = 'unknown error', $code = null,
+                        $mode = null, $options = null, $userinfo = null)
+    {
+        if ($mode === null) {
+            $mode = PEAR_ERROR_RETURN;
+        }
+        $this->message   = $message;
+        $this->code      = $code;
+        $this->mode      = $mode;
+        $this->userinfo  = $userinfo;
+        if (!PEAR::getStaticProperty('PEAR_Error', 'skiptrace')) {
+            $this->backtrace = debug_backtrace();
+            if (isset($this->backtrace[0]) && isset($this->backtrace[0]['object'])) {
+                unset($this->backtrace[0]['object']);
+            }
+        }
+        if ($mode & PEAR_ERROR_CALLBACK) {
+            $this->level = E_USER_NOTICE;
+            $this->callback = $options;
+        } else {
+            if ($options === null) {
+                $options = E_USER_NOTICE;
+            }
+            $this->level = $options;
+            $this->callback = null;
+        }
+        if ($this->mode & PEAR_ERROR_PRINT) {
+            if (is_null($options) || is_int($options)) {
+                $format = "%s";
+            } else {
+                $format = $options;
+            }
+            printf($format, $this->getMessage());
+        }
+        if ($this->mode & PEAR_ERROR_TRIGGER) {
+            trigger_error($this->getMessage(), $this->level);
+        }
+        if ($this->mode & PEAR_ERROR_DIE) {
+            $msg = $this->getMessage();
+            if (is_null($options) || is_int($options)) {
+                $format = "%s";
+                if (substr($msg, -1) != "\n") {
+                    $msg .= "\n";
+                }
+            } else {
+                $format = $options;
+            }
+            die(sprintf($format, $msg));
+        }
+        if ($this->mode & PEAR_ERROR_CALLBACK) {
+            if (is_callable($this->callback)) {
+                call_user_func($this->callback, $this);
+            }
+        }
+        if ($this->mode & PEAR_ERROR_EXCEPTION) {
+            trigger_error("PEAR_ERROR_EXCEPTION is obsolete, use class PEAR_Exception for exceptions", E_USER_WARNING);
+            eval('$e = new Exception($this->message, $this->code);throw($e);');
+        }
+    }
+
+    // }}}
+    // {{{ getMode()
+
+    /**
+     * Get the error mode from an error object.
+     *
+     * @return int error mode
+     * @access public
+     */
+    function getMode() {
+        return $this->mode;
+    }
+
+    // }}}
+    // {{{ getCallback()
+
+    /**
+     * Get the callback function/method from an error object.
+     *
+     * @return mixed callback function or object/method array
+     * @access public
+     */
+    function getCallback() {
+        return $this->callback;
+    }
+
+    // }}}
+    // {{{ getMessage()
+
+
+    /**
+     * Get the error message from an error object.
+     *
+     * @return  string  full error message
+     * @access public
+     */
+    function getMessage()
+    {
+        return ($this->error_message_prefix . $this->message);
+    }
+
+
+    // }}}
+    // {{{ getCode()
+
+    /**
+     * Get error code from an error object
+     *
+     * @return int error code
+     * @access public
+     */
+     function getCode()
+     {
+        return $this->code;
+     }
+
+    // }}}
+    // {{{ getType()
+
+    /**
+     * Get the name of this error/exception.
+     *
+     * @return string error/exception name (type)
+     * @access public
+     */
+    function getType()
+    {
+        return get_class($this);
+    }
+
+    // }}}
+    // {{{ getUserInfo()
+
+    /**
+     * Get additional user-supplied information.
+     *
+     * @return string user-supplied information
+     * @access public
+     */
+    function getUserInfo()
+    {
+        return $this->userinfo;
+    }
+
+    // }}}
+    // {{{ getDebugInfo()
+
+    /**
+     * Get additional debug information supplied by the application.
+     *
+     * @return string debug information
+     * @access public
+     */
+    function getDebugInfo()
+    {
+        return $this->getUserInfo();
+    }
+
+    // }}}
+    // {{{ getBacktrace()
+
+    /**
+     * Get the call backtrace from where the error was generated.
+     * Supported with PHP 4.3.0 or newer.
+     *
+     * @param int $frame (optional) what frame to fetch
+     * @return array Backtrace, or NULL if not available.
+     * @access public
+     */
+    function getBacktrace($frame = null)
+    {
+        if (defined('PEAR_IGNORE_BACKTRACE')) {
+            return null;
+        }
+        if ($frame === null) {
+            return $this->backtrace;
+        }
+        return $this->backtrace[$frame];
+    }
+
+    // }}}
+    // {{{ addUserInfo()
+
+    function addUserInfo($info)
+    {
+        if (empty($this->userinfo)) {
+            $this->userinfo = $info;
+        } else {
+            $this->userinfo .= " ** $info";
+        }
+    }
+
+    // }}}
+    // {{{ toString()
+
+    /**
+     * Make a string representation of this object.
+     *
+     * @return string a string with an object summary
+     * @access public
+     */
+    function toString() {
+        $modes = array();
+        $levels = array(E_USER_NOTICE  => 'notice',
+                        E_USER_WARNING => 'warning',
+                        E_USER_ERROR   => 'error');
+        if ($this->mode & PEAR_ERROR_CALLBACK) {
+            if (is_array($this->callback)) {
+                $callback = (is_object($this->callback[0]) ?
+                    strtolower(get_class($this->callback[0])) :
+                    $this->callback[0]) . '::' .
+                    $this->callback[1];
+            } else {
+                $callback = $this->callback;
+            }
+            return sprintf('[%s: message="%s" code=%d mode=callback '.
+                           'callback=%s prefix="%s" info="%s"]',
+                           strtolower(get_class($this)), $this->message, $this->code,
+                           $callback, $this->error_message_prefix,
+                           $this->userinfo);
+        }
+        if ($this->mode & PEAR_ERROR_PRINT) {
+            $modes[] = 'print';
+        }
+        if ($this->mode & PEAR_ERROR_TRIGGER) {
+            $modes[] = 'trigger';
+        }
+        if ($this->mode & PEAR_ERROR_DIE) {
+            $modes[] = 'die';
+        }
+        if ($this->mode & PEAR_ERROR_RETURN) {
+            $modes[] = 'return';
+        }
+        return sprintf('[%s: message="%s" code=%d mode=%s level=%s '.
+                       'prefix="%s" info="%s"]',
+                       strtolower(get_class($this)), $this->message, $this->code,
+                       implode("|", $modes), $levels[$this->level],
+                       $this->error_message_prefix,
+                       $this->userinfo);
+    }
+
+    // }}}
+}
+
+/*
+ * Local Variables:
+ * mode: php
+ * tab-width: 4
+ * c-basic-offset: 4
+ * End:
+ */
+?>
diff --git a/lib/PEAR/Exception.php b/lib/PEAR/Exception.php
new file mode 100644 (file)
index 0000000..b8bba0e
--- /dev/null
@@ -0,0 +1,397 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 foldmethod=marker: */
+/**
+ * PEAR_Exception
+ *
+ * PHP versions 4 and 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   pear
+ * @package    PEAR
+ * @author     Tomas V. V. Cox <cox@idecnet.com>
+ * @author     Hans Lellelid <hans@velum.net>
+ * @author     Bertrand Mansion <bmansion@mamasam.com>
+ * @author     Greg Beaver <cellog@php.net>
+ * @copyright  1997-2006 The PHP Group
+ * @license    http://www.php.net/license/3_0.txt  PHP License 3.0
+ * @version    CVS: $Id: Exception.php,v 1.28 2007/05/07 01:58:54 cellog Exp $
+ * @link       http://pear.php.net/package/PEAR
+ * @since      File available since Release 1.3.3
+ */
+
+
+/**
+ * Base PEAR_Exception Class
+ *
+ * 1) Features:
+ *
+ * - Nestable exceptions (throw new PEAR_Exception($msg, $prev_exception))
+ * - Definable triggers, shot when exceptions occur
+ * - Pretty and informative error messages
+ * - Added more context info available (like class, method or cause)
+ * - cause can be a PEAR_Exception or an array of mixed
+ *   PEAR_Exceptions/PEAR_ErrorStack warnings
+ * - callbacks for specific exception classes and their children
+ *
+ * 2) Ideas:
+ *
+ * - Maybe a way to define a 'template' for the output
+ *
+ * 3) Inherited properties from PHP Exception Class:
+ *
+ * protected $message
+ * protected $code
+ * protected $line
+ * protected $file
+ * private   $trace
+ *
+ * 4) Inherited methods from PHP Exception Class:
+ *
+ * __clone
+ * __construct
+ * getMessage
+ * getCode
+ * getFile
+ * getLine
+ * getTraceSafe
+ * getTraceSafeAsString
+ * __toString
+ *
+ * 5) Usage example
+ *
+ * <code>
+ *  require_once 'PEAR/Exception.php';
+ *
+ *  class Test {
+ *     function foo() {
+ *         throw new PEAR_Exception('Error Message', ERROR_CODE);
+ *     }
+ *  }
+ *
+ *  function myLogger($pear_exception) {
+ *     echo $pear_exception->getMessage();
+ *  }
+ *  // each time a exception is thrown the 'myLogger' will be called
+ *  // (its use is completely optional)
+ *  PEAR_Exception::addObserver('myLogger');
+ *  $test = new Test;
+ *  try {
+ *     $test->foo();
+ *  } catch (PEAR_Exception $e) {
+ *     print $e;
+ *  }
+ * </code>
+ *
+ * @category   pear
+ * @package    PEAR
+ * @author     Tomas V.V.Cox <cox@idecnet.com>
+ * @author     Hans Lellelid <hans@velum.net>
+ * @author     Bertrand Mansion <bmansion@mamasam.com>
+ * @author     Greg Beaver <cellog@php.net>
+ * @copyright  1997-2006 The PHP Group
+ * @license    http://www.php.net/license/3_0.txt  PHP License 3.0
+ * @version    Release: 1.6.2
+ * @link       http://pear.php.net/package/PEAR
+ * @since      Class available since Release 1.3.3
+ *
+ */
+class PEAR_Exception extends Exception
+{
+    const OBSERVER_PRINT = -2;
+    const OBSERVER_TRIGGER = -4;
+    const OBSERVER_DIE = -8;
+    protected $cause;
+    private static $_observers = array();
+    private static $_uniqueid = 0;
+    private $_trace;
+
+    /**
+     * Supported signatures:
+     *  - PEAR_Exception(string $message);
+     *  - PEAR_Exception(string $message, int $code);
+     *  - PEAR_Exception(string $message, Exception $cause);
+     *  - PEAR_Exception(string $message, Exception $cause, int $code);
+     *  - PEAR_Exception(string $message, PEAR_Error $cause);
+     *  - PEAR_Exception(string $message, PEAR_Error $cause, int $code);
+     *  - PEAR_Exception(string $message, array $causes);
+     *  - PEAR_Exception(string $message, array $causes, int $code);
+     * @param string exception message
+     * @param int|Exception|PEAR_Error|array|null exception cause
+     * @param int|null exception code or null
+     */
+    public function __construct($message, $p2 = null, $p3 = null)
+    {
+        if (is_int($p2)) {
+            $code = $p2;
+            $this->cause = null;
+        } elseif (is_object($p2) || is_array($p2)) {
+            // using is_object allows both Exception and PEAR_Error
+            if (is_object($p2) && !($p2 instanceof Exception)) {
+                if (!class_exists('PEAR_Error') || !($p2 instanceof PEAR_Error)) {
+                    throw new PEAR_Exception('exception cause must be Exception, ' .
+                        'array, or PEAR_Error');
+                }
+            }
+            $code = $p3;
+            if (is_array($p2) && isset($p2['message'])) {
+                // fix potential problem of passing in a single warning
+                $p2 = array($p2);
+            }
+            $this->cause = $p2;
+        } else {
+            $code = null;
+            $this->cause = null;
+        }
+        parent::__construct($message, $code);
+        $this->signal();
+    }
+
+    /**
+     * @param mixed $callback  - A valid php callback, see php func is_callable()
+     *                         - A PEAR_Exception::OBSERVER_* constant
+     *                         - An array(const PEAR_Exception::OBSERVER_*,
+     *                           mixed $options)
+     * @param string $label    The name of the observer. Use this if you want
+     *                         to remove it later with removeObserver()
+     */
+    public static function addObserver($callback, $label = 'default')
+    {
+        self::$_observers[$label] = $callback;
+    }
+
+    public static function removeObserver($label = 'default')
+    {
+        unset(self::$_observers[$label]);
+    }
+
+    /**
+     * @return int unique identifier for an observer
+     */
+    public static function getUniqueId()
+    {
+        return self::$_uniqueid++;
+    }
+
+    private function signal()
+    {
+        foreach (self::$_observers as $func) {
+            if (is_callable($func)) {
+                call_user_func($func, $this);
+                continue;
+            }
+            settype($func, 'array');
+            switch ($func[0]) {
+                case self::OBSERVER_PRINT :
+                    $f = (isset($func[1])) ? $func[1] : '%s';
+                    printf($f, $this->getMessage());
+                    break;
+                case self::OBSERVER_TRIGGER :
+                    $f = (isset($func[1])) ? $func[1] : E_USER_NOTICE;
+                    trigger_error($this->getMessage(), $f);
+                    break;
+                case self::OBSERVER_DIE :
+                    $f = (isset($func[1])) ? $func[1] : '%s';
+                    die(printf($f, $this->getMessage()));
+                    break;
+                default:
+                    trigger_error('invalid observer type', E_USER_WARNING);
+            }
+        }
+    }
+
+    /**
+     * Return specific error information that can be used for more detailed
+     * error messages or translation.
+     *
+     * This method may be overridden in child exception classes in order
+     * to add functionality not present in PEAR_Exception and is a placeholder
+     * to define API
+     *
+     * The returned array must be an associative array of parameter => value like so:
+     * <pre>
+     * array('name' => $name, 'context' => array(...))
+     * </pre>
+     * @return array
+     */
+    public function getErrorData()
+    {
+        return array();
+    }
+
+    /**
+     * Returns the exception that caused this exception to be thrown
+     * @access public
+     * @return Exception|array The context of the exception
+     */
+    public function getCause()
+    {
+        return $this->cause;
+    }
+
+    /**
+     * Function must be public to call on caused exceptions
+     * @param array
+     */
+    public function getCauseMessage(&$causes)
+    {
+        $trace = $this->getTraceSafe();
+        $cause = array('class'   => get_class($this),
+                       'message' => $this->message,
+                       'file' => 'unknown',
+                       'line' => 'unknown');
+        if (isset($trace[0])) {
+            if (isset($trace[0]['file'])) {
+                $cause['file'] = $trace[0]['file'];
+                $cause['line'] = $trace[0]['line'];
+            }
+        }
+        $causes[] = $cause;
+        if ($this->cause instanceof PEAR_Exception) {
+            $this->cause->getCauseMessage($causes);
+        } elseif ($this->cause instanceof Exception) {
+            $causes[] = array('class'   => get_class($this->cause),
+                              'message' => $this->cause->getMessage(),
+                              'file' => $this->cause->getFile(),
+                              'line' => $this->cause->getLine());
+        } elseif (class_exists('PEAR_Error') && $this->cause instanceof PEAR_Error) {
+            $causes[] = array('class' => get_class($this->cause),
+                              'message' => $this->cause->getMessage(),
+                              'file' => 'unknown',
+                              'line' => 'unknown');
+        } elseif (is_array($this->cause)) {
+            foreach ($this->cause as $cause) {
+                if ($cause instanceof PEAR_Exception) {
+                    $cause->getCauseMessage($causes);
+                } elseif ($cause instanceof Exception) {
+                    $causes[] = array('class'   => get_class($cause),
+                                   'message' => $cause->getMessage(),
+                                   'file' => $cause->getFile(),
+                                   'line' => $cause->getLine());
+                } elseif (class_exists('PEAR_Error') && $cause instanceof PEAR_Error) {
+                    $causes[] = array('class' => get_class($cause),
+                                      'message' => $cause->getMessage(),
+                                      'file' => 'unknown',
+                                      'line' => 'unknown');
+                } elseif (is_array($cause) && isset($cause['message'])) {
+                    // PEAR_ErrorStack warning
+                    $causes[] = array(
+                        'class' => $cause['package'],
+                        'message' => $cause['message'],
+                        'file' => isset($cause['context']['file']) ?
+                                            $cause['context']['file'] :
+                                            'unknown',
+                        'line' => isset($cause['context']['line']) ?
+                                            $cause['context']['line'] :
+                                            'unknown',
+                    );
+                }
+            }
+        }
+    }
+
+    public function getTraceSafe()
+    {   
+        if (!isset($this->_trace)) {
+            $this->_trace = $this->getTrace();
+            if (empty($this->_trace)) {
+                $backtrace = debug_backtrace();
+                $this->_trace = array($backtrace[count($backtrace)-1]);
+            }
+        }
+        return $this->_trace;
+    }
+
+    public function getErrorClass()
+    {
+        $trace = $this->getTraceSafe();
+        return $trace[0]['class'];
+    }
+
+    public function getErrorMethod()
+    {
+        $trace = $this->getTraceSafe();
+        return $trace[0]['function'];
+    }
+
+    public function __toString()
+    {
+        if (isset($_SERVER['REQUEST_URI'])) {
+            return $this->toHtml();
+        }
+        return $this->toText();
+    }
+
+    public function toHtml()
+    {
+        $trace = $this->getTraceSafe();
+        $causes = array();
+        $this->getCauseMessage($causes);
+        $html =  '<table border="1" cellspacing="0">' . "\n";
+        foreach ($causes as $i => $cause) {
+            $html .= '<tr><td colspan="3" bgcolor="#ff9999">'
+               . str_repeat('-', $i) . ' <b>' . $cause['class'] . '</b>: '
+               . htmlspecialchars($cause['message']) . ' in <b>' . $cause['file'] . '</b> '
+               . 'on line <b>' . $cause['line'] . '</b>'
+               . "</td></tr>\n";
+        }
+        $html .= '<tr><td colspan="3" bgcolor="#aaaaaa" align="center"><b>Exception trace</b></td></tr>' . "\n"
+               . '<tr><td align="center" bgcolor="#cccccc" width="20"><b>#</b></td>'
+               . '<td align="center" bgcolor="#cccccc"><b>Function</b></td>'
+               . '<td align="center" bgcolor="#cccccc"><b>Location</b></td></tr>' . "\n";
+
+        foreach ($trace as $k => $v) {
+            $html .= '<tr><td align="center">' . $k . '</td>'
+                   . '<td>';
+            if (!empty($v['class'])) {
+                $html .= $v['class'] . $v['type'];
+            }
+            $html .= $v['function'];
+            $args = array();
+            if (!empty($v['args'])) {
+                foreach ($v['args'] as $arg) {
+                    if (is_null($arg)) $args[] = 'null';
+                    elseif (is_array($arg)) $args[] = 'Array';
+                    elseif (is_object($arg)) $args[] = 'Object('.get_class($arg).')';
+                    elseif (is_bool($arg)) $args[] = $arg ? 'true' : 'false';
+                    elseif (is_int($arg) || is_double($arg)) $args[] = $arg;
+                    else {
+                        $arg = (string)$arg;
+                        $str = htmlspecialchars(substr($arg, 0, 16));
+                        if (strlen($arg) > 16) $str .= '&hellip;';
+                        $args[] = "'" . $str . "'";
+                    }
+                }
+            }
+            $html .= '(' . implode(', ',$args) . ')'
+                   . '</td>'
+                   . '<td>' . (isset($v['file']) ? $v['file'] : 'unknown')
+                   . ':' . (isset($v['line']) ? $v['line'] : 'unknown')
+                   . '</td></tr>' . "\n";
+        }
+        $html .= '<tr><td align="center">' . ($k+1) . '</td>'
+               . '<td>{main}</td>'
+               . '<td>&nbsp;</td></tr>' . "\n"
+               . '</table>';
+        return $html;
+    }
+
+    public function toText()
+    {
+        $causes = array();
+        $this->getCauseMessage($causes);
+        $causeMsg = '';
+        foreach ($causes as $i => $cause) {
+            $causeMsg .= str_repeat(' ', $i) . $cause['class'] . ': '
+                   . $cause['message'] . ' in ' . $cause['file']
+                   . ' on line ' . $cause['line'] . "\n";
+        }
+        return $causeMsg . $this->getTraceAsString();
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/Snoopy.class.php b/lib/Snoopy.class.php
new file mode 100644 (file)
index 0000000..77e5b73
--- /dev/null
@@ -0,0 +1,1257 @@
+<?php
+
+/*************************************************
+
+Snoopy - the PHP net client
+Author: Monte Ohrt <monte@ispi.net>
+Copyright (c): 1999-2000 ispi, all rights reserved
+Version: 1.01
+
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+You may contact the author of Snoopy by e-mail at:
+monte@ispi.net
+
+Or, write to:
+Monte Ohrt
+CTO, ispi
+237 S. 70th suite 220
+Lincoln, NE 68510
+
+The latest version of Snoopy can be obtained from:
+http://snoopy.sourceforge.net/
+
+*************************************************/
+
+class Snoopy
+{
+       /**** Public variables ****/
+       
+       /* user definable vars */
+
+       var $host                       =       "www.php.net";          // host name we are connecting to
+       var $port                       =       80;                                     // port we are connecting to
+       var $proxy_host         =       "";                                     // proxy host to use
+       var $proxy_port         =       "";                                     // proxy port to use
+       var $proxy_user         =       "";                                     // proxy user to use
+       var $proxy_pass         =       "";                                     // proxy password to use
+       
+       var $agent                      =       "Snoopy v1.2.3";        // agent we masquerade as
+       var     $referer                =       "";                                     // referer info to pass
+       var $cookies            =       array();                        // array of cookies to pass
+                                                                                               // $cookies["username"]="joe";
+       var     $rawheaders             =       array();                        // array of raw headers to send
+                                                                                               // $rawheaders["Content-type"]="text/html";
+
+       var $maxredirs          =       5;                                      // http redirection depth maximum. 0 = disallow
+       var $lastredirectaddr   =       "";                             // contains address of last redirected address
+       var     $offsiteok              =       true;                           // allows redirection off-site
+       var $maxframes          =       0;                                      // frame content depth maximum. 0 = disallow
+       var $expandlinks        =       true;                           // expand links to fully qualified URLs.
+                                                                                               // this only applies to fetchlinks()
+                                                                                               // submitlinks(), and submittext()
+       var $passcookies        =       true;                           // pass set cookies back through redirects
+                                                                                               // NOTE: this currently does not respect
+                                                                                               // dates, domains or paths.
+       
+       var     $user                   =       "";                                     // user for http authentication
+       var     $pass                   =       "";                                     // password for http authentication
+       
+       // http accept types
+       var $accept                     =       "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*";
+       
+       var $results            =       "";                                     // where the content is put
+               
+       var $error                      =       "";                                     // error messages sent here
+       var     $response_code  =       "";                                     // response code returned from server
+       var     $headers                =       array();                        // headers returned from server sent here
+       var     $maxlength              =       500000;                         // max return data length (body)
+       var $read_timeout       =       0;                                      // timeout on read operations, in seconds
+                                                                                               // supported only since PHP 4 Beta 4
+                                                                                               // set to 0 to disallow timeouts
+       var $timed_out          =       false;                          // if a read operation timed out
+       var     $status                 =       0;                                      // http request status
+
+       var $temp_dir           =       "/tmp";                         // temporary directory that the webserver
+                                                                                               // has permission to write to.
+                                                                                               // under Windows, this should be C:\temp
+
+       var     $curl_path              =       "/usr/local/bin/curl";
+                                                                                               // Snoopy will use cURL for fetching
+                                                                                               // SSL content if a full system path to
+                                                                                               // the cURL binary is supplied here.
+                                                                                               // set to false if you do not have
+                                                                                               // cURL installed. See http://curl.haxx.se
+                                                                                               // for details on installing cURL.
+                                                                                               // Snoopy does *not* use the cURL
+                                                                                               // library functions built into php,
+                                                                                               // as these functions are not stable
+                                                                                               // as of this Snoopy release.
+       
+       /**** Private variables ****/   
+       
+       var     $_maxlinelen    =       4096;                           // max line length (headers)
+       
+       var $_httpmethod        =       "GET";                          // default http request method
+       var $_httpversion       =       "HTTP/1.0";                     // default http request version
+       var $_submit_method     =       "POST";                         // default submit method
+       var $_submit_type       =       "application/x-www-form-urlencoded";    // default submit type
+       var $_mime_boundary     =   "";                                 // MIME boundary for multipart/form-data submit type
+       var $_redirectaddr      =       false;                          // will be set if page fetched is a redirect
+       var $_redirectdepth     =       0;                                      // increments on an http redirect
+       var $_frameurls         =       array();                        // frame src urls
+       var $_framedepth        =       0;                                      // increments on frame depth
+       
+       var $_isproxy           =       false;                          // set if using a proxy server
+       var $_fp_timeout        =       30;                                     // timeout for socket connection
+
+/*======================================================================*\
+       Function:       fetch
+       Purpose:        fetch the contents of a web page
+                               (and possibly other protocols in the
+                               future like ftp, nntp, gopher, etc.)
+       Input:          $URI    the location of the page to fetch
+       Output:         $this->results  the output text from the fetch
+\*======================================================================*/
+
+       function fetch($URI)
+       {
+       
+               //preg_match("|^([^:]+)://([^:/]+)(:[\d]+)*(.*)|",$URI,$URI_PARTS);
+               $URI_PARTS = parse_url($URI);
+               if (!empty($URI_PARTS["user"]))
+                       $this->user = $URI_PARTS["user"];
+               if (!empty($URI_PARTS["pass"]))
+                       $this->pass = $URI_PARTS["pass"];
+               if (empty($URI_PARTS["query"]))
+                       $URI_PARTS["query"] = '';
+               if (empty($URI_PARTS["path"]))
+                       $URI_PARTS["path"] = '';
+                               
+               switch(strtolower($URI_PARTS["scheme"]))
+               {
+                       case "http":
+                               $this->host = $URI_PARTS["host"];
+                               if(!empty($URI_PARTS["port"]))
+                                       $this->port = $URI_PARTS["port"];
+                               if($this->_connect($fp))
+                               {
+                                       if($this->_isproxy)
+                                       {
+                                               // using proxy, send entire URI
+                                               $this->_httprequest($URI,$fp,$URI,$this->_httpmethod);
+                                       }
+                                       else
+                                       {
+                                               $path = $URI_PARTS["path"].($URI_PARTS["query"] ? "?".$URI_PARTS["query"] : "");
+                                               // no proxy, send only the path
+                                               $this->_httprequest($path, $fp, $URI, $this->_httpmethod);
+                                       }
+                                       
+                                       $this->_disconnect($fp);
+
+                                       if($this->_redirectaddr)
+                                       {
+                                               /* url was redirected, check if we've hit the max depth */
+                                               if($this->maxredirs > $this->_redirectdepth)
+                                               {
+                                                       // only follow redirect if it's on this site, or offsiteok is true
+                                                       if(preg_match("|^http://".preg_quote($this->host)."|i",$this->_redirectaddr) || $this->offsiteok)
+                                                       {
+                                                               /* follow the redirect */
+                                                               $this->_redirectdepth++;
+                                                               $this->lastredirectaddr=$this->_redirectaddr;
+                                                               $this->fetch($this->_redirectaddr);
+                                                       }
+                                               }
+                                       }
+
+                                       if($this->_framedepth < $this->maxframes && count($this->_frameurls) > 0)
+                                       {
+                                               $frameurls = $this->_frameurls;
+                                               $this->_frameurls = array();
+                                               
+                                               while(list(,$frameurl) = each($frameurls))
+                                               {
+                                                       if($this->_framedepth < $this->maxframes)
+                                                       {
+                                                               $this->fetch($frameurl);
+                                                               $this->_framedepth++;
+                                                       }
+                                                       else
+                                                               break;
+                                               }
+                                       }                                       
+                               }
+                               else
+                               {
+                                       return false;
+                               }
+                               return true;                                    
+                               break;
+                       case "https":
+                               if(!$this->curl_path)
+                                       return false;
+                               if(function_exists("is_executable"))
+                                   if (!is_executable($this->curl_path))
+                                       return false;
+                               $this->host = $URI_PARTS["host"];
+                               if(!empty($URI_PARTS["port"]))
+                                       $this->port = $URI_PARTS["port"];
+                               if($this->_isproxy)
+                               {
+                                       // using proxy, send entire URI
+                                       $this->_httpsrequest($URI,$URI,$this->_httpmethod);
+                               }
+                               else
+                               {
+                                       $path = $URI_PARTS["path"].($URI_PARTS["query"] ? "?".$URI_PARTS["query"] : "");
+                                       // no proxy, send only the path
+                                       $this->_httpsrequest($path, $URI, $this->_httpmethod);
+                               }
+
+                               if($this->_redirectaddr)
+                               {
+                                       /* url was redirected, check if we've hit the max depth */
+                                       if($this->maxredirs > $this->_redirectdepth)
+                                       {
+                                               // only follow redirect if it's on this site, or offsiteok is true
+                                               if(preg_match("|^http://".preg_quote($this->host)."|i",$this->_redirectaddr) || $this->offsiteok)
+                                               {
+                                                       /* follow the redirect */
+                                                       $this->_redirectdepth++;
+                                                       $this->lastredirectaddr=$this->_redirectaddr;
+                                                       $this->fetch($this->_redirectaddr);
+                                               }
+                                       }
+                               }
+
+                               if($this->_framedepth < $this->maxframes && count($this->_frameurls) > 0)
+                               {
+                                       $frameurls = $this->_frameurls;
+                                       $this->_frameurls = array();
+
+                                       while(list(,$frameurl) = each($frameurls))
+                                       {
+                                               if($this->_framedepth < $this->maxframes)
+                                               {
+                                                       $this->fetch($frameurl);
+                                                       $this->_framedepth++;
+                                               }
+                                               else
+                                                       break;
+                                       }
+                               }                                       
+                               return true;                                    
+                               break;
+                       default:
+                               // not a valid protocol
+                               $this->error    =       'Invalid protocol "'.$URI_PARTS["scheme"].'"\n';
+                               return false;
+                               break;
+               }               
+               return true;
+       }
+
+/*======================================================================*\
+       Function:       submit
+       Purpose:        submit an http form
+       Input:          $URI    the location to post the data
+                               $formvars       the formvars to use.
+                                       format: $formvars["var"] = "val";
+                               $formfiles  an array of files to submit
+                                       format: $formfiles["var"] = "/dir/filename.ext";
+       Output:         $this->results  the text output from the post
+\*======================================================================*/
+
+       function submit($URI, $formvars="", $formfiles="")
+       {
+               unset($postdata);
+               
+               $postdata = $this->_prepare_post_body($formvars, $formfiles);
+                       
+               $URI_PARTS = parse_url($URI);
+               if (!empty($URI_PARTS["user"]))
+                       $this->user = $URI_PARTS["user"];
+               if (!empty($URI_PARTS["pass"]))
+                       $this->pass = $URI_PARTS["pass"];
+               if (empty($URI_PARTS["query"]))
+                       $URI_PARTS["query"] = '';
+               if (empty($URI_PARTS["path"]))
+                       $URI_PARTS["path"] = '';
+
+               switch(strtolower($URI_PARTS["scheme"]))
+               {
+                       case "http":
+                               $this->host = $URI_PARTS["host"];
+                               if(!empty($URI_PARTS["port"]))
+                                       $this->port = $URI_PARTS["port"];
+                               if($this->_connect($fp))
+                               {
+                                       if($this->_isproxy)
+                                       {
+                                               // using proxy, send entire URI
+                                               $this->_httprequest($URI,$fp,$URI,$this->_submit_method,$this->_submit_type,$postdata);
+                                       }
+                                       else
+                                       {
+                                               $path = $URI_PARTS["path"].($URI_PARTS["query"] ? "?".$URI_PARTS["query"] : "");
+                                               // no proxy, send only the path
+                                               $this->_httprequest($path, $fp, $URI, $this->_submit_method, $this->_submit_type, $postdata);
+                                       }
+                                       
+                                       $this->_disconnect($fp);
+
+                                       if($this->_redirectaddr)
+                                       {
+                                               /* url was redirected, check if we've hit the max depth */
+                                               if($this->maxredirs > $this->_redirectdepth)
+                                               {                                               
+                                                       if(!preg_match("|^".$URI_PARTS["scheme"]."://|", $this->_redirectaddr))
+                                                               $this->_redirectaddr = $this->_expandlinks($this->_redirectaddr,$URI_PARTS["scheme"]."://".$URI_PARTS["host"]);                                         
+                                                       
+                                                       // only follow redirect if it's on this site, or offsiteok is true
+                                                       if(preg_match("|^http://".preg_quote($this->host)."|i",$this->_redirectaddr) || $this->offsiteok)
+                                                       {
+                                                               /* follow the redirect */
+                                                               $this->_redirectdepth++;
+                                                               $this->lastredirectaddr=$this->_redirectaddr;
+                                                               if( strpos( $this->_redirectaddr, "?" ) > 0 )
+                                                                       $this->fetch($this->_redirectaddr); // the redirect has changed the request method from post to get
+                                                               else
+                                                                       $this->submit($this->_redirectaddr,$formvars, $formfiles);
+                                                       }
+                                               }
+                                       }
+
+                                       if($this->_framedepth < $this->maxframes && count($this->_frameurls) > 0)
+                                       {
+                                               $frameurls = $this->_frameurls;
+                                               $this->_frameurls = array();
+                                               
+                                               while(list(,$frameurl) = each($frameurls))
+                                               {                                                                                                               
+                                                       if($this->_framedepth < $this->maxframes)
+                                                       {
+                                                               $this->fetch($frameurl);
+                                                               $this->_framedepth++;
+                                                       }
+                                                       else
+                                                               break;
+                                               }
+                                       }                                       
+                                       
+                               }
+                               else
+                               {
+                                       return false;
+                               }
+                               return true;                                    
+                               break;
+                       case "https":
+                               if(!$this->curl_path)
+                                       return false;
+                               if(function_exists("is_executable"))
+                                   if (!is_executable($this->curl_path))
+                                       return false;
+                               $this->host = $URI_PARTS["host"];
+                               if(!empty($URI_PARTS["port"]))
+                                       $this->port = $URI_PARTS["port"];
+                               if($this->_isproxy)
+                               {
+                                       // using proxy, send entire URI
+                                       $this->_httpsrequest($URI, $URI, $this->_submit_method, $this->_submit_type, $postdata);
+                               }
+                               else
+                               {
+                                       $path = $URI_PARTS["path"].($URI_PARTS["query"] ? "?".$URI_PARTS["query"] : "");
+                                       // no proxy, send only the path
+                                       $this->_httpsrequest($path, $URI, $this->_submit_method, $this->_submit_type, $postdata);
+                               }
+
+                               if($this->_redirectaddr)
+                               {
+                                       /* url was redirected, check if we've hit the max depth */
+                                       if($this->maxredirs > $this->_redirectdepth)
+                                       {                                               
+                                               if(!preg_match("|^".$URI_PARTS["scheme"]."://|", $this->_redirectaddr))
+                                                       $this->_redirectaddr = $this->_expandlinks($this->_redirectaddr,$URI_PARTS["scheme"]."://".$URI_PARTS["host"]);                                         
+
+                                               // only follow redirect if it's on this site, or offsiteok is true
+                                               if(preg_match("|^http://".preg_quote($this->host)."|i",$this->_redirectaddr) || $this->offsiteok)
+                                               {
+                                                       /* follow the redirect */
+                                                       $this->_redirectdepth++;
+                                                       $this->lastredirectaddr=$this->_redirectaddr;
+                                                       if( strpos( $this->_redirectaddr, "?" ) > 0 )
+                                                               $this->fetch($this->_redirectaddr); // the redirect has changed the request method from post to get
+                                                       else
+                                                               $this->submit($this->_redirectaddr,$formvars, $formfiles);
+                                               }
+                                       }
+                               }
+
+                               if($this->_framedepth < $this->maxframes && count($this->_frameurls) > 0)
+                               {
+                                       $frameurls = $this->_frameurls;
+                                       $this->_frameurls = array();
+
+                                       while(list(,$frameurl) = each($frameurls))
+                                       {                                                                                                               
+                                               if($this->_framedepth < $this->maxframes)
+                                               {
+                                                       $this->fetch($frameurl);
+                                                       $this->_framedepth++;
+                                               }
+                                               else
+                                                       break;
+                                       }
+                               }                                       
+                               return true;                                    
+                               break;
+                               
+                       default:
+                               // not a valid protocol
+                               $this->error    =       'Invalid protocol "'.$URI_PARTS["scheme"].'"\n';
+                               return false;
+                               break;
+               }               
+               return true;
+       }
+
+/*======================================================================*\
+       Function:       fetchlinks
+       Purpose:        fetch the links from a web page
+       Input:          $URI    where you are fetching from
+       Output:         $this->results  an array of the URLs
+\*======================================================================*/
+
+       function fetchlinks($URI)
+       {
+               if ($this->fetch($URI))
+               {                       
+                       if($this->lastredirectaddr)
+                               $URI = $this->lastredirectaddr;
+                       if(is_array($this->results))
+                       {
+                               for($x=0;$x<count($this->results);$x++)
+                                       $this->results[$x] = $this->_striplinks($this->results[$x]);
+                       }
+                       else
+                               $this->results = $this->_striplinks($this->results);
+
+                       if($this->expandlinks)
+                               $this->results = $this->_expandlinks($this->results, $URI);
+                       return true;
+               }
+               else
+                       return false;
+       }
+
+/*======================================================================*\
+       Function:       fetchform
+       Purpose:        fetch the form elements from a web page
+       Input:          $URI    where you are fetching from
+       Output:         $this->results  the resulting html form
+\*======================================================================*/
+
+       function fetchform($URI)
+       {
+               
+               if ($this->fetch($URI))
+               {                       
+
+                       if(is_array($this->results))
+                       {
+                               for($x=0;$x<count($this->results);$x++)
+                                       $this->results[$x] = $this->_stripform($this->results[$x]);
+                       }
+                       else
+                               $this->results = $this->_stripform($this->results);
+                       
+                       return true;
+               }
+               else
+                       return false;
+       }
+       
+       
+/*======================================================================*\
+       Function:       fetchtext
+       Purpose:        fetch the text from a web page, stripping the links
+       Input:          $URI    where you are fetching from
+       Output:         $this->results  the text from the web page
+\*======================================================================*/
+
+       function fetchtext($URI)
+       {
+               if($this->fetch($URI))
+               {                       
+                       if(is_array($this->results))
+                       {
+                               for($x=0;$x<count($this->results);$x++)
+                                       $this->results[$x] = $this->_striptext($this->results[$x]);
+                       }
+                       else
+                               $this->results = $this->_striptext($this->results);
+                       return true;
+               }
+               else
+                       return false;
+       }
+
+/*======================================================================*\
+       Function:       submitlinks
+       Purpose:        grab links from a form submission
+       Input:          $URI    where you are submitting from
+       Output:         $this->results  an array of the links from the post
+\*======================================================================*/
+
+       function submitlinks($URI, $formvars="", $formfiles="")
+       {
+               if($this->submit($URI,$formvars, $formfiles))
+               {                       
+                       if($this->lastredirectaddr)
+                               $URI = $this->lastredirectaddr;
+                       if(is_array($this->results))
+                       {
+                               for($x=0;$x<count($this->results);$x++)
+                               {
+                                       $this->results[$x] = $this->_striplinks($this->results[$x]);
+                                       if($this->expandlinks)
+                                               $this->results[$x] = $this->_expandlinks($this->results[$x],$URI);
+                               }
+                       }
+                       else
+                       {
+                               $this->results = $this->_striplinks($this->results);
+                               if($this->expandlinks)
+                                       $this->results = $this->_expandlinks($this->results,$URI);
+                       }
+                       return true;
+               }
+               else
+                       return false;
+       }
+
+/*======================================================================*\
+       Function:       submittext
+       Purpose:        grab text from a form submission
+       Input:          $URI    where you are submitting from
+       Output:         $this->results  the text from the web page
+\*======================================================================*/
+
+       function submittext($URI, $formvars = "", $formfiles = "")
+       {
+               if($this->submit($URI,$formvars, $formfiles))
+               {                       
+                       if($this->lastredirectaddr)
+                               $URI = $this->lastredirectaddr;
+                       if(is_array($this->results))
+                       {
+                               for($x=0;$x<count($this->results);$x++)
+                               {
+                                       $this->results[$x] = $this->_striptext($this->results[$x]);
+                                       if($this->expandlinks)
+                                               $this->results[$x] = $this->_expandlinks($this->results[$x],$URI);
+                               }
+                       }
+                       else
+                       {
+                               $this->results = $this->_striptext($this->results);
+                               if($this->expandlinks)
+                                       $this->results = $this->_expandlinks($this->results,$URI);
+                       }
+                       return true;
+               }
+               else
+                       return false;
+       }
+
+       
+
+/*======================================================================*\
+       Function:       set_submit_multipart
+       Purpose:        Set the form submission content type to
+                               multipart/form-data
+\*======================================================================*/
+       function set_submit_multipart()
+       {
+               $this->_submit_type = "multipart/form-data";
+       }
+
+       
+/*======================================================================*\
+       Function:       set_submit_normal
+       Purpose:        Set the form submission content type to
+                               application/x-www-form-urlencoded
+\*======================================================================*/
+       function set_submit_normal()
+       {
+               $this->_submit_type = "application/x-www-form-urlencoded";
+       }
+
+       
+       
+
+/*======================================================================*\
+       Private functions
+\*======================================================================*/
+       
+       
+/*======================================================================*\
+       Function:       _striplinks
+       Purpose:        strip the hyperlinks from an html document
+       Input:          $document       document to strip.
+       Output:         $match          an array of the links
+\*======================================================================*/
+
+       function _striplinks($document)
+       {       
+               preg_match_all("'<\s*a\s.*?href\s*=\s*                  # find <a href=
+                                               ([\"\'])?                                       # find single or double quote
+                                               (?(1) (.*?)\\1 | ([^\s\>]+))            # if quote found, match up to next matching
+                                                                                                       # quote, otherwise match up to next space
+                                               'isx",$document,$links);
+                                               
+
+               // catenate the non-empty matches from the conditional subpattern
+
+               while(list($key,$val) = each($links[2]))
+               {
+                       if(!empty($val))
+                               $match[] = $val;
+               }                               
+               
+               while(list($key,$val) = each($links[3]))
+               {
+                       if(!empty($val))
+                               $match[] = $val;
+               }               
+               
+               // return the links
+               return $match;
+       }
+
+/*======================================================================*\
+       Function:       _stripform
+       Purpose:        strip the form elements from an html document
+       Input:          $document       document to strip.
+       Output:         $match          an array of the links
+\*======================================================================*/
+
+       function _stripform($document)
+       {       
+               preg_match_all("'<\/?(FORM|INPUT|SELECT|TEXTAREA|(OPTION))[^<>]*>(?(2)(.*(?=<\/?(option|select)[^<>]*>[\r\n]*)|(?=[\r\n]*))|(?=[\r\n]*))'Usi",$document,$elements);
+               
+               // catenate the matches
+               $match = implode("\r\n",$elements[0]);
+                               
+               // return the links
+               return $match;
+       }
+
+       
+       
+/*======================================================================*\
+       Function:       _striptext
+       Purpose:        strip the text from an html document
+       Input:          $document       document to strip.
+       Output:         $text           the resulting text
+\*======================================================================*/
+
+       function _striptext($document)
+       {
+               
+               // I didn't use preg eval (//e) since that is only available in PHP 4.0.
+               // so, list your entities one by one here. I included some of the
+               // more common ones.
+                                                               
+               $search = array("'<script[^>]*?>.*?</script>'si",       // strip out javascript
+                                               "'<[\/\!]*?[^<>]*?>'si",                        // strip out html tags
+                                               "'([\r\n])[\s]+'",                                      // strip out white space
+                                               "'&(quot|#34|#034|#x22);'i",            // replace html entities
+                                               "'&(amp|#38|#038|#x26);'i",                     // added hexadecimal values
+                                               "'&(lt|#60|#060|#x3c);'i",
+                                               "'&(gt|#62|#062|#x3e);'i",
+                                               "'&(nbsp|#160|#xa0);'i",
+                                               "'&(iexcl|#161);'i",
+                                               "'&(cent|#162);'i",
+                                               "'&(pound|#163);'i",
+                                               "'&(copy|#169);'i",
+                                               "'&(reg|#174);'i",
+                                               "'&(deg|#176);'i",
+                                               "'&(#39|#039|#x27);'",
+                                               "'&(euro|#8364);'i",                            // europe
+                                               "'&a(uml|UML);'",                                       // german
+                                               "'&o(uml|UML);'",
+                                               "'&u(uml|UML);'",
+                                               "'&A(uml|UML);'",
+                                               "'&O(uml|UML);'",
+                                               "'&U(uml|UML);'",
+                                               "'&szlig;'i",
+                                               );
+               $replace = array(       "",
+                                                       "",
+                                                       "\\1",
+                                                       "\"",
+                                                       "&",
+                                                       "<",
+                                                       ">",
+                                                       " ",
+                                                       chr(161),
+                                                       chr(162),
+                                                       chr(163),
+                                                       chr(169),
+                                                       chr(174),
+                                                       chr(176),
+                                                       chr(39),
+                                                       chr(128),
+                                                       "ä",
+                                                       "ö",
+                                                       "ü",
+                                                       "Ä",
+                                                       "Ö",
+                                                       "Ü",
+                                                       "ß",
+                                               );
+                                       
+               $text = preg_replace($search,$replace,$document);
+                                                               
+               return $text;
+       }
+
+/*======================================================================*\
+       Function:       _expandlinks
+       Purpose:        expand each link into a fully qualified URL
+       Input:          $links                  the links to qualify
+                               $URI                    the full URI to get the base from
+       Output:         $expandedLinks  the expanded links
+\*======================================================================*/
+
+       function _expandlinks($links,$URI)
+       {
+               
+               preg_match("/^[^\?]+/",$URI,$match);
+
+               $match = preg_replace("|/[^\/\.]+\.[^\/\.]+$|","",$match[0]);
+               $match = preg_replace("|/$|","",$match);
+               $match_part = parse_url($match);
+               $match_root =
+               $match_part["scheme"]."://".$match_part["host"];
+                               
+               $search = array(        "|^http://".preg_quote($this->host)."|i",
+                                                       "|^(\/)|i",
+                                                       "|^(?!http://)(?!mailto:)|i",
+                                                       "|/\./|",
+                                                       "|/[^\/]+/\.\./|"
+                                               );
+                                               
+               $replace = array(       "",
+                                                       $match_root."/",
+                                                       $match."/",
+                                                       "/",
+                                                       "/"
+                                               );                      
+                               
+               $expandedLinks = preg_replace($search,$replace,$links);
+
+               return $expandedLinks;
+       }
+
+/*======================================================================*\
+       Function:       _httprequest
+       Purpose:        go get the http data from the server
+       Input:          $url            the url to fetch
+                               $fp                     the current open file pointer
+                               $URI            the full URI
+                               $body           body contents to send if any (POST)
+       Output:         
+\*======================================================================*/
+       
+       function _httprequest($url,$fp,$URI,$http_method,$content_type="",$body="")
+       {
+               $cookie_headers = '';
+               if($this->passcookies && $this->_redirectaddr)
+                       $this->setcookies();
+                       
+               $URI_PARTS = parse_url($URI);
+               if(empty($url))
+                       $url = "/";
+               $headers = $http_method." ".$url." ".$this->_httpversion."\r\n";                
+               if(!empty($this->agent))
+                       $headers .= "User-Agent: ".$this->agent."\r\n";
+               if(!empty($this->host) && !isset($this->rawheaders['Host'])) {
+                       $headers .= "Host: ".$this->host;
+                       if(!empty($this->port))
+                               $headers .= ":".$this->port;
+                       $headers .= "\r\n";
+               }
+               if(!empty($this->accept))
+                       $headers .= "Accept: ".$this->accept."\r\n";
+               if(!empty($this->referer))
+                       $headers .= "Referer: ".$this->referer."\r\n";
+               if(!empty($this->cookies))
+               {                       
+                       if(!is_array($this->cookies))
+                               $this->cookies = (array)$this->cookies;
+       
+                       reset($this->cookies);
+                       if ( count($this->cookies) > 0 ) {
+                               $cookie_headers .= 'Cookie: ';
+                               foreach ( $this->cookies as $cookieKey => $cookieVal ) {
+                               $cookie_headers .= $cookieKey."=".urlencode($cookieVal)."; ";
+                               }
+                               $headers .= substr($cookie_headers,0,-2) . "\r\n";
+                       } 
+               }
+               if(!empty($this->rawheaders))
+               {
+                       if(!is_array($this->rawheaders))
+                               $this->rawheaders = (array)$this->rawheaders;
+                       while(list($headerKey,$headerVal) = each($this->rawheaders))
+                               $headers .= $headerKey.": ".$headerVal."\r\n";
+               }
+               if(!empty($content_type)) {
+                       $headers .= "Content-type: $content_type";
+                       if ($content_type == "multipart/form-data")
+                               $headers .= "; boundary=".$this->_mime_boundary;
+                       $headers .= "\r\n";
+               }
+               if(!empty($body))       
+                       $headers .= "Content-length: ".strlen($body)."\r\n";
+               if(!empty($this->user) || !empty($this->pass))  
+                       $headers .= "Authorization: Basic ".base64_encode($this->user.":".$this->pass)."\r\n";
+               
+               //add proxy auth headers
+               if(!empty($this->proxy_user))   
+                       $headers .= 'Proxy-Authorization: ' . 'Basic ' . base64_encode($this->proxy_user . ':' . $this->proxy_pass)."\r\n";
+
+
+               $headers .= "\r\n";
+               
+               // set the read timeout if needed
+               if ($this->read_timeout > 0)
+                       socket_set_timeout($fp, $this->read_timeout);
+               $this->timed_out = false;
+               
+               fwrite($fp,$headers.$body,strlen($headers.$body));
+               
+               $this->_redirectaddr = false;
+               unset($this->headers);
+                                               
+               while($currentHeader = fgets($fp,$this->_maxlinelen))
+               {
+                       if ($this->read_timeout > 0 && $this->_check_timeout($fp))
+                       {
+                               $this->status=-100;
+                               return false;
+                       }
+                               
+                       if($currentHeader == "\r\n")
+                               break;
+                                               
+                       // if a header begins with Location: or URI:, set the redirect
+                       if(preg_match("/^(Location:|URI:)/i",$currentHeader))
+                       {
+                               // get URL portion of the redirect
+                               preg_match("/^(Location:|URI:)[ ]+(.*)/i",chop($currentHeader),$matches);
+                               // look for :// in the Location header to see if hostname is included
+                               if(!preg_match("|\:\/\/|",$matches[2]))
+                               {
+                                       // no host in the path, so prepend
+                                       $this->_redirectaddr = $URI_PARTS["scheme"]."://".$this->host.":".$this->port;
+                                       // eliminate double slash
+                                       if(!preg_match("|^/|",$matches[2]))
+                                                       $this->_redirectaddr .= "/".$matches[2];
+                                       else
+                                                       $this->_redirectaddr .= $matches[2];
+                               }
+                               else
+                                       $this->_redirectaddr = $matches[2];
+                       }
+               
+                       if(preg_match("|^HTTP/|",$currentHeader))
+                       {
+                if(preg_match("|^HTTP/[^\s]*\s(.*?)\s|",$currentHeader, $status))
+                               {
+                                       $this->status= $status[1];
+                }                              
+                               $this->response_code = $currentHeader;
+                       }
+                               
+                       $this->headers[] = $currentHeader;
+               }
+
+               $results = '';
+               do {
+               $_data = fread($fp, $this->maxlength);
+               if (strlen($_data) == 0) {
+                       break;
+               }
+               $results .= $_data;
+               } while(true);
+
+               if ($this->read_timeout > 0 && $this->_check_timeout($fp))
+               {
+                       $this->status=-100;
+                       return false;
+               }
+               
+               // check if there is a a redirect meta tag
+               
+               if(preg_match("'<meta[\s]*http-equiv[^>]*?content[\s]*=[\s]*[\"\']?\d+;[\s]*URL[\s]*=[\s]*([^\"\']*?)[\"\']?>'i",$results,$match))
+
+               {
+                       $this->_redirectaddr = $this->_expandlinks($match[1],$URI);     
+               }
+
+               // have we hit our frame depth and is there frame src to fetch?
+               if(($this->_framedepth < $this->maxframes) && preg_match_all("'<frame\s+.*src[\s]*=[\'\"]?([^\'\"\>]+)'i",$results,$match))
+               {
+                       $this->results[] = $results;
+                       for($x=0; $x<count($match[1]); $x++)
+                               $this->_frameurls[] = $this->_expandlinks($match[1][$x],$URI_PARTS["scheme"]."://".$this->host);
+               }
+               // have we already fetched framed content?
+               elseif(is_array($this->results))
+                       $this->results[] = $results;
+               // no framed content
+               else
+                       $this->results = $results;
+               
+               return true;
+       }
+
+/*======================================================================*\
+       Function:       _httpsrequest
+       Purpose:        go get the https data from the server using curl
+       Input:          $url            the url to fetch
+                               $URI            the full URI
+                               $body           body contents to send if any (POST)
+       Output:         
+\*======================================================================*/
+       
+       function _httpsrequest($url,$URI,$http_method,$content_type="",$body="")
+       {
+               if($this->passcookies && $this->_redirectaddr)
+                       $this->setcookies();
+
+               $headers = array();             
+                                       
+               $URI_PARTS = parse_url($URI);
+               if(empty($url))
+                       $url = "/";
+               // GET ... header not needed for curl
+               //$headers[] = $http_method." ".$url." ".$this->_httpversion;           
+               if(!empty($this->agent))
+                       $headers[] = "User-Agent: ".$this->agent;
+               if(!empty($this->host))
+                       if(!empty($this->port))
+                               $headers[] = "Host: ".$this->host.":".$this->port;
+                       else
+                               $headers[] = "Host: ".$this->host;
+               if(!empty($this->accept))
+                       $headers[] = "Accept: ".$this->accept;
+               if(!empty($this->referer))
+                       $headers[] = "Referer: ".$this->referer;
+               if(!empty($this->cookies))
+               {                       
+                       if(!is_array($this->cookies))
+                               $this->cookies = (array)$this->cookies;
+       
+                       reset($this->cookies);
+                       if ( count($this->cookies) > 0 ) {
+                               $cookie_str = 'Cookie: ';
+                               foreach ( $this->cookies as $cookieKey => $cookieVal ) {
+                               $cookie_str .= $cookieKey."=".urlencode($cookieVal)."; ";
+                               }
+                               $headers[] = substr($cookie_str,0,-2);
+                       }
+               }
+               if(!empty($this->rawheaders))
+               {
+                       if(!is_array($this->rawheaders))
+                               $this->rawheaders = (array)$this->rawheaders;
+                       while(list($headerKey,$headerVal) = each($this->rawheaders))
+                               $headers[] = $headerKey.": ".$headerVal;
+               }
+               if(!empty($content_type)) {
+                       if ($content_type == "multipart/form-data")
+                               $headers[] = "Content-type: $content_type; boundary=".$this->_mime_boundary;
+                       else
+                               $headers[] = "Content-type: $content_type";
+               }
+               if(!empty($body))       
+                       $headers[] = "Content-length: ".strlen($body);
+               if(!empty($this->user) || !empty($this->pass))  
+                       $headers[] = "Authorization: BASIC ".base64_encode($this->user.":".$this->pass);
+                       
+               for($curr_header = 0; $curr_header < count($headers); $curr_header++) {
+                       $safer_header = strtr( $headers[$curr_header], "\"", " " );
+                       $cmdline_params .= " -H \"".$safer_header."\"";
+               }
+               
+               if(!empty($body))
+                       $cmdline_params .= " -d \"$body\"";
+               
+               if($this->read_timeout > 0)
+                       $cmdline_params .= " -m ".$this->read_timeout;
+               
+               $headerfile = tempnam($temp_dir, "sno");
+
+               $safer_URI = strtr( $URI, "\"", " " ); // strip quotes from the URI to avoid shell access
+               exec($this->curl_path." -D \"$headerfile\"".$cmdline_params." \"".$safer_URI."\"",$results,$return);
+               
+               if($return)
+               {
+                       $this->error = "Error: cURL could not retrieve the document, error $return.";
+                       return false;
+               }
+                       
+                       
+               $results = implode("\r\n",$results);
+               
+               $result_headers = file("$headerfile");
+                                               
+               $this->_redirectaddr = false;
+               unset($this->headers);
+                                               
+               for($currentHeader = 0; $currentHeader < count($result_headers); $currentHeader++)
+               {
+                       
+                       // if a header begins with Location: or URI:, set the redirect
+                       if(preg_match("/^(Location: |URI: )/i",$result_headers[$currentHeader]))
+                       {
+                               // get URL portion of the redirect
+                               preg_match("/^(Location: |URI:)\s+(.*)/",chop($result_headers[$currentHeader]),$matches);
+                               // look for :// in the Location header to see if hostname is included
+                               if(!preg_match("|\:\/\/|",$matches[2]))
+                               {
+                                       // no host in the path, so prepend
+                                       $this->_redirectaddr = $URI_PARTS["scheme"]."://".$this->host.":".$this->port;
+                                       // eliminate double slash
+                                       if(!preg_match("|^/|",$matches[2]))
+                                                       $this->_redirectaddr .= "/".$matches[2];
+                                       else
+                                                       $this->_redirectaddr .= $matches[2];
+                               }
+                               else
+                                       $this->_redirectaddr = $matches[2];
+                       }
+               
+                       if(preg_match("|^HTTP/|",$result_headers[$currentHeader]))
+                               $this->response_code = $result_headers[$currentHeader];
+
+                       $this->headers[] = $result_headers[$currentHeader];
+               }
+
+               // check if there is a a redirect meta tag
+               
+               if(preg_match("'<meta[\s]*http-equiv[^>]*?content[\s]*=[\s]*[\"\']?\d+;[\s]*URL[\s]*=[\s]*([^\"\']*?)[\"\']?>'i",$results,$match))
+               {
+                       $this->_redirectaddr = $this->_expandlinks($match[1],$URI);     
+               }
+
+               // have we hit our frame depth and is there frame src to fetch?
+               if(($this->_framedepth < $this->maxframes) && preg_match_all("'<frame\s+.*src[\s]*=[\'\"]?([^\'\"\>]+)'i",$results,$match))
+               {
+                       $this->results[] = $results;
+                       for($x=0; $x<count($match[1]); $x++)
+                               $this->_frameurls[] = $this->_expandlinks($match[1][$x],$URI_PARTS["scheme"]."://".$this->host);
+               }
+               // have we already fetched framed content?
+               elseif(is_array($this->results))
+                       $this->results[] = $results;
+               // no framed content
+               else
+                       $this->results = $results;
+
+               unlink("$headerfile");
+               
+               return true;
+       }
+
+/*======================================================================*\
+       Function:       setcookies()
+       Purpose:        set cookies for a redirection
+\*======================================================================*/
+       
+       function setcookies()
+       {
+               for($x=0; $x<count($this->headers); $x++)
+               {
+               if(preg_match('/^set-cookie:[\s]+([^=]+)=([^;]+)/i', $this->headers[$x],$match))
+                       $this->cookies[$match[1]] = urldecode($match[2]);
+               }
+       }
+
+       
+/*======================================================================*\
+       Function:       _check_timeout
+       Purpose:        checks whether timeout has occurred
+       Input:          $fp     file pointer
+\*======================================================================*/
+
+       function _check_timeout($fp)
+       {
+               if ($this->read_timeout > 0) {
+                       $fp_status = socket_get_status($fp);
+                       if ($fp_status["timed_out"]) {
+                               $this->timed_out = true;
+                               return true;
+                       }
+               }
+               return false;
+       }
+
+/*======================================================================*\
+       Function:       _connect
+       Purpose:        make a socket connection
+       Input:          $fp     file pointer
+\*======================================================================*/
+       
+       function _connect(&$fp)
+       {
+               if(!empty($this->proxy_host) && !empty($this->proxy_port))
+                       {
+                               $this->_isproxy = true;
+                               
+                               $host = $this->proxy_host;
+                               $port = $this->proxy_port;
+                       }
+               else
+               {
+                       $host = $this->host;
+                       $port = $this->port;
+               }
+       
+               $this->status = 0;
+               
+               if($fp = fsockopen(
+                                       $host,
+                                       $port,
+                                       $errno,
+                                       $errstr,
+                                       $this->_fp_timeout
+                                       ))
+               {
+                       // socket connection succeeded
+
+                       return true;
+               }
+               else
+               {
+                       // socket connection failed
+                       $this->status = $errno;
+                       switch($errno)
+                       {
+                               case -3:
+                                       $this->error="socket creation failed (-3)";
+                               case -4:
+                                       $this->error="dns lookup failure (-4)";
+                               case -5:
+                                       $this->error="connection refused or timed out (-5)";
+                               default:
+                                       $this->error="connection failed (".$errno.")";
+                       }
+                       return false;
+               }
+       }
+/*======================================================================*\
+       Function:       _disconnect
+       Purpose:        disconnect a socket connection
+       Input:          $fp     file pointer
+\*======================================================================*/
+       
+       function _disconnect($fp)
+       {
+               return(fclose($fp));
+       }
+
+       
+/*======================================================================*\
+       Function:       _prepare_post_body
+       Purpose:        Prepare post body according to encoding type
+       Input:          $formvars  - form variables
+                               $formfiles - form upload files
+       Output:         post body
+\*======================================================================*/
+       
+       function _prepare_post_body($formvars, $formfiles)
+       {
+               settype($formvars, "array");
+               settype($formfiles, "array");
+               $postdata = '';
+
+               if (count($formvars) == 0 && count($formfiles) == 0)
+                       return;
+               
+               switch ($this->_submit_type) {
+                       case "application/x-www-form-urlencoded":
+                               reset($formvars);
+                               while(list($key,$val) = each($formvars)) {
+                                       if (is_array($val) || is_object($val)) {
+                                               while (list($cur_key, $cur_val) = each($val)) {
+                                                       $postdata .= urlencode($key)."[]=".urlencode($cur_val)."&";
+                                               }
+                                       } else
+                                               $postdata .= urlencode($key)."=".urlencode($val)."&";
+                               }
+                               break;
+
+                       case "multipart/form-data":
+                               $this->_mime_boundary = "Snoopy".md5(uniqid(microtime()));
+                               
+                               reset($formvars);
+                               while(list($key,$val) = each($formvars)) {
+                                       if (is_array($val) || is_object($val)) {
+                                               while (list($cur_key, $cur_val) = each($val)) {
+                                                       $postdata .= "--".$this->_mime_boundary."\r\n";
+                                                       $postdata .= "Content-Disposition: form-data; name=\"$key\[\]\"\r\n\r\n";
+                                                       $postdata .= "$cur_val\r\n";
+                                               }
+                                       } else {
+                                               $postdata .= "--".$this->_mime_boundary."\r\n";
+                                               $postdata .= "Content-Disposition: form-data; name=\"$key\"\r\n\r\n";
+                                               $postdata .= "$val\r\n";
+                                       }
+                               }
+                               
+                               reset($formfiles);
+                               while (list($field_name, $file_names) = each($formfiles)) {
+                                       settype($file_names, "array");
+                                       while (list(, $file_name) = each($file_names)) {
+                                               if (!is_readable($file_name)) continue;
+
+                                               $fp = fopen($file_name, "r");
+                                               $file_content = fread($fp, filesize($file_name));
+                                               fclose($fp);
+                                               $base_name = basename($file_name);
+
+                                               $postdata .= "--".$this->_mime_boundary."\r\n";
+                                               $postdata .= "Content-Disposition: form-data; name=\"$field_name\"; filename=\"$base_name\"\r\n\r\n";
+                                               $postdata .= "$file_content\r\n";
+                                       }
+                               }
+                               $postdata .= "--".$this->_mime_boundary."--\r\n";
+                               break;
+               }
+
+               return $postdata;
+       }
+}
+
+?>
diff --git a/lib/XML/Feed/Parser.php b/lib/XML/Feed/Parser.php
new file mode 100644 (file)
index 0000000..67668c4
--- /dev/null
@@ -0,0 +1,351 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Key gateway class for XML_Feed_Parser package
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL
+ * @version    CVS: $Id: Parser.php,v 1.24 2006/08/15 13:04:00 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/**
+ * XML_Feed_Parser_Type is an abstract class required by all of our
+ * feed types. It makes sense to load it here to keep the other files
+ * clean.
+ */
+require_once 'XML/Feed/Parser/Type.php';
+
+/**
+ * We will throw exceptions when errors occur.
+ */
+require_once 'XML/Feed/Parser/Exception.php';
+
+/**
+ * This is the core of the XML_Feed_Parser package. It identifies feed types 
+ * and abstracts access to them. It is an iterator, allowing for easy access 
+ * to the entire feed.
+ *
+ * @author  James Stewart <james@jystewart.net>
+ * @version Release: 1.0.2
+ * @package XML_Feed_Parser
+ */
+class XML_Feed_Parser implements Iterator
+{
+    /**
+     * This is where we hold the feed object 
+     * @var Object
+     */
+    private $feed;
+
+    /**
+     * To allow for extensions, we make a public reference to the feed model 
+     * @var DOMDocument
+     */
+    public $model;
+    
+    /**
+     * A map between entry ID and offset
+     * @var array
+     */
+    protected $idMappings = array();
+
+    /**
+     * A storage space for Namespace URIs.
+     * @var array
+     */
+    private $feedNamespaces = array(
+        'rss2' => array(
+            'http://backend.userland.com/rss',
+            'http://backend.userland.com/rss2',
+            'http://blogs.law.harvard.edu/tech/rss'));
+    /**
+     * Detects feed types and instantiate appropriate objects.
+     *
+     * Our constructor takes care of detecting feed types and instantiating
+     * appropriate classes. For now we're going to treat Atom 0.3 as Atom 1.0
+     * but raise a warning. I do not intend to introduce full support for 
+     * Atom 0.3 as it has been deprecated, but others are welcome to.
+     *
+     * @param    string    $feed    XML serialization of the feed
+     * @param    bool    $strict    Whether or not to validate the feed
+     * @param    bool    $suppressWarnings Trigger errors for deprecated feed types?
+     * @param    bool    $tidy    Whether or not to try and use the tidy library on input
+     */
+    function __construct($feed, $strict = false, $suppressWarnings = false, $tidy = false)
+    {
+        $this->model = new DOMDocument;
+        if (! $this->model->loadXML($feed)) {
+            if (extension_loaded('tidy') && $tidy) {
+                $tidy = new tidy;
+                $tidy->parseString($feed, 
+                    array('input-xml' => true, 'output-xml' => true));
+                $tidy->cleanRepair();
+                if (! $this->model->loadXML((string) $tidy)) {
+                    throw new XML_Feed_Parser_Exception('Invalid input: this is not ' .
+                        'valid XML');
+                }
+            } else {
+                throw new XML_Feed_Parser_Exception('Invalid input: this is not valid XML');
+            }
+
+        }
+
+        /* detect feed type */
+        $doc_element = $this->model->documentElement;
+        $error = false;
+
+        switch (true) {
+            case ($doc_element->namespaceURI == 'http://www.w3.org/2005/Atom'):
+                require_once 'XML/Feed/Parser/Atom.php';
+                require_once 'XML/Feed/Parser/AtomElement.php';
+                $class = 'XML_Feed_Parser_Atom';
+                break;
+            case ($doc_element->namespaceURI == 'http://purl.org/atom/ns#'):
+                require_once 'XML/Feed/Parser/Atom.php';
+                require_once 'XML/Feed/Parser/AtomElement.php';
+                $class = 'XML_Feed_Parser_Atom';
+                $error = 'Atom 0.3 deprecated, using 1.0 parser which won\'t provide ' .
+                    'all options';
+                break;
+            case ($doc_element->namespaceURI == 'http://purl.org/rss/1.0/' || 
+                ($doc_element->hasChildNodes() && $doc_element->childNodes->length > 1 
+                && $doc_element->childNodes->item(1)->namespaceURI == 
+                'http://purl.org/rss/1.0/')):
+                require_once 'XML/Feed/Parser/RSS1.php';
+                require_once 'XML/Feed/Parser/RSS1Element.php';
+                $class = 'XML_Feed_Parser_RSS1';
+                break;
+            case ($doc_element->namespaceURI == 'http://purl.org/rss/1.1/' || 
+                ($doc_element->hasChildNodes() && $doc_element->childNodes->length > 1 
+                && $doc_element->childNodes->item(1)->namespaceURI == 
+                'http://purl.org/rss/1.1/')):
+                require_once 'XML/Feed/Parser/RSS11.php';
+                require_once 'XML/Feed/Parser/RSS11Element.php';
+                $class = 'XML_Feed_Parser_RSS11';
+                break;
+            case (($doc_element->hasChildNodes() && $doc_element->childNodes->length > 1
+                && $doc_element->childNodes->item(1)->namespaceURI == 
+                'http://my.netscape.com/rdf/simple/0.9/') || 
+                $doc_element->namespaceURI == 'http://my.netscape.com/rdf/simple/0.9/'):
+                require_once 'XML/Feed/Parser/RSS09.php';
+                require_once 'XML/Feed/Parser/RSS09Element.php';
+                $class = 'XML_Feed_Parser_RSS09';
+                break;
+            case ($doc_element->tagName == 'rss' and
+                $doc_element->hasAttribute('version') && 
+                $doc_element->getAttribute('version') == 0.91):
+                $error = 'RSS 0.91 has been superceded by RSS2.0. Using RSS2.0 parser.';
+                require_once 'XML/Feed/Parser/RSS2.php';
+                require_once 'XML/Feed/Parser/RSS2Element.php';
+                $class = 'XML_Feed_Parser_RSS2';
+                break;
+            case ($doc_element->tagName == 'rss' and
+                $doc_element->hasAttribute('version') && 
+                $doc_element->getAttribute('version') == 0.92):
+                $error = 'RSS 0.92 has been superceded by RSS2.0. Using RSS2.0 parser.';
+                require_once 'XML/Feed/Parser/RSS2.php';
+                require_once 'XML/Feed/Parser/RSS2Element.php';
+                $class = 'XML_Feed_Parser_RSS2';
+                break;
+            case (in_array($doc_element->namespaceURI, $this->feedNamespaces['rss2'])
+                || $doc_element->tagName == 'rss'):
+                if (! $doc_element->hasAttribute('version') || 
+                    $doc_element->getAttribute('version') != 2) {
+                    $error = 'RSS version not specified. Parsing as RSS2.0';
+                }
+                require_once 'XML/Feed/Parser/RSS2.php';
+                require_once 'XML/Feed/Parser/RSS2Element.php';
+                $class = 'XML_Feed_Parser_RSS2';
+                break;
+            default:
+                throw new XML_Feed_Parser_Exception('Feed type unknown');
+                break;
+        }
+
+        if (! $suppressWarnings && ! empty($error)) {
+            trigger_error($error, E_USER_WARNING);
+        }
+
+        /* Instantiate feed object */
+        $this->feed = new $class($this->model, $strict);
+    }
+
+    /**
+     * Proxy to allow feed element names to be used as method names
+     *
+     * For top-level feed elements we will provide access using methods or 
+     * attributes. This function simply passes on a request to the appropriate 
+     * feed type object.
+     *
+     * @param   string  $call - the method being called
+     * @param   array   $attributes
+     */
+    function __call($call, $attributes)
+    {
+        $attributes = array_pad($attributes, 5, false);
+        list($a, $b, $c, $d, $e) = $attributes;
+        return $this->feed->$call($a, $b, $c, $d, $e);
+    }
+
+    /**
+     * Proxy to allow feed element names to be used as attribute names
+     *
+     * To allow variable-like access to feed-level data we use this
+     * method. It simply passes along to __call() which in turn passes
+     * along to the relevant object.
+     *
+     * @param   string  $val - the name of the variable required
+     */
+    function __get($val)
+    {
+        return $this->feed->$val;
+    }
+
+    /**
+     * Provides iteration functionality.
+     *
+     * Of course we must be able to iterate... This function simply increases
+     * our internal counter.
+     */
+    function next()
+    {
+        if (isset($this->current_item) && 
+            $this->current_item <= $this->feed->numberEntries - 1) {
+            ++$this->current_item;
+        } else if (! isset($this->current_item)) {
+            $this->current_item = 0;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Return XML_Feed_Type object for current element
+     *
+     * @return    XML_Feed_Parser_Type Object
+     */
+    function current()
+    {
+        return $this->getEntryByOffset($this->current_item);
+    }
+
+    /**
+     * For iteration -- returns the key for the current stage in the array.
+     *
+     * @return    int
+     */    
+    function key()
+    {
+        return $this->current_item;
+    }
+
+    /**
+     * For iteration -- tells whether we have reached the 
+     * end.
+     *
+     * @return    bool
+     */
+    function valid()
+    {
+        return $this->current_item < $this->feed->numberEntries;
+    }
+
+    /**
+     * For iteration -- resets the internal counter to the beginning.
+     */
+    function rewind()
+    {
+        $this->current_item = 0;
+    }
+
+    /**
+     * Provides access to entries by ID if one is specified in the source feed.
+     *
+     * As well as allowing the items to be iterated over we want to allow
+     * users to be able to access a specific entry. This is one of two ways of
+     * doing that, the other being by offset. This method can be quite slow
+     * if dealing with a large feed that hasn't yet been processed as it
+     * instantiates objects for every entry until it finds the one needed.
+     *
+     * @param    string    $id  Valid ID for the given feed format
+     * @return    XML_Feed_Parser_Type|false
+     */            
+    function getEntryById($id)
+    {
+        if (isset($this->idMappings[$id])) {
+            return $this->getEntryByOffset($this->idMappings[$id]);
+        }
+
+        /* 
+         * Since we have not yet encountered that ID, let's go through all the
+         * remaining entries in order till we find it.
+         * This is a fairly slow implementation, but it should work.
+         */
+        return $this->feed->getEntryById($id);
+    }
+
+    /**
+     * Retrieve entry by numeric offset, starting from zero.
+     *
+     * As well as allowing the items to be iterated over we want to allow
+     * users to be able to access a specific entry. This is one of two ways of
+     * doing that, the other being by ID.
+     *
+     * @param    int    $offset The position of the entry within the feed, starting from 0
+     * @return    XML_Feed_Parser_Type|false
+     */
+    function getEntryByOffset($offset)
+    {
+        if ($offset < $this->feed->numberEntries) {
+            if (isset($this->feed->entries[$offset])) {
+                return $this->feed->entries[$offset];
+            } else {
+                try {
+                    $this->feed->getEntryByOffset($offset);
+                } catch (Exception $e) {
+                    return false;
+                }
+                $id = $this->feed->entries[$offset]->getID();
+                $this->idMappings[$id] = $offset;
+                return $this->feed->entries[$offset];
+            }
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Retrieve version details from feed type class.
+     *
+     * @return void
+     * @author James Stewart
+     */
+    function version()
+    {
+        return $this->feed->version;
+    }
+    
+    /**
+     * Returns a string representation of the feed.
+     * 
+     * @return String
+     **/
+    function __toString()
+    {
+        return $this->feed->__toString();
+    }
+}
+?>
\ No newline at end of file
diff --git a/lib/XML/Feed/Parser/Atom.php b/lib/XML/Feed/Parser/Atom.php
new file mode 100644 (file)
index 0000000..9b70bea
--- /dev/null
@@ -0,0 +1,365 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Atom feed class for XML_Feed_Parser
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: Atom.php,v 1.25 2007/03/26 12:49:05 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+*/
+
+/**
+ * This is the class that determines how we manage Atom 1.0 feeds
+ * 
+ * How we deal with constructs:
+ *  date - return as unix datetime for use with the 'date' function unless specified otherwise
+ *  text - return as is. optional parameter will give access to attributes
+ *  person - defaults to name, but parameter based access
+ *
+ * @author    James Stewart <james@jystewart.net>
+ * @version    Release: 1.0.2
+ * @package XML_Feed_Parser
+ */
+class XML_Feed_Parser_Atom extends XML_Feed_Parser_Type
+{
+    /**
+     * The URI of the RelaxNG schema used to (optionally) validate the feed 
+     * @var string
+     */
+    private $relax = 'atom.rnc';
+
+    /**
+     * We're likely to use XPath, so let's keep it global 
+     * @var DOMXPath
+     */
+    public $xpath;
+
+    /**
+     * When performing XPath queries we will use this prefix 
+     * @var string
+     */
+    private $xpathPrefix = '//';
+
+    /**
+     * The feed type we are parsing 
+     * @var string
+     */
+    public $version = 'Atom 1.0';
+
+    /** 
+     * The class used to represent individual items 
+     * @var string
+     */
+    protected $itemClass = 'XML_Feed_Parser_AtomElement';
+    
+    /** 
+     * The element containing entries 
+     * @var string
+     */
+    protected $itemElement = 'entry';
+
+    /**
+     * Here we map those elements we're not going to handle individually
+     * to the constructs they are. The optional second parameter in the array
+     * tells the parser whether to 'fall back' (not apt. at the feed level) or
+     * fail if the element is missing. If the parameter is not set, the function
+     * will simply return false and leave it to the client to decide what to do.
+     * @var array
+     */
+    protected $map = array(
+        'author' => array('Person'),
+        'contributor' => array('Person'),
+        'icon' => array('Text'),
+        'logo' => array('Text'),
+        'id' => array('Text', 'fail'),
+        'rights' => array('Text'),
+        'subtitle' => array('Text'),
+        'title' => array('Text', 'fail'),
+        'updated' => array('Date', 'fail'),
+        'link' => array('Link'),
+        'generator' => array('Text'),
+        'category' => array('Category'));
+
+    /**
+     * Here we provide a few mappings for those very special circumstances in
+     * which it makes sense to map back to the RSS2 spec. Key is RSS2 version
+     * value is an array consisting of the equivalent in atom and any attributes
+     * needed to make the mapping.
+     * @var array
+     */
+    protected $compatMap = array(
+        'guid' => array('id'),
+        'links' => array('link'),
+        'tags' => array('category'),
+        'contributors' => array('contributor'));
+
+    /**
+     * Our constructor does nothing more than its parent.
+     * 
+     * @param    DOMDocument    $xml    A DOM object representing the feed
+     * @param    bool (optional) $string    Whether or not to validate this feed
+     */
+    function __construct(DOMDocument $model, $strict = false)
+    {
+        $this->model = $model;
+
+        if ($strict) {
+            if (! $this->model->relaxNGValidateSource($this->relax)) {
+                throw new XML_Feed_Parser_Exception('Failed required validation');
+            }
+        }
+
+        $this->xpath = new DOMXPath($this->model);
+        $this->xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
+        $this->numberEntries = $this->count('entry');
+    }
+
+    /**
+     * Implement retrieval of an entry based on its ID for atom feeds.
+     *
+     * This function uses XPath to get the entry based on its ID. If DOMXPath::evaluate
+     * is available, we also use that to store a reference to the entry in the array
+     * used by getEntryByOffset so that method does not have to seek out the entry
+     * if it's requested that way.
+     * 
+     * @param    string    $id    any valid Atom ID.
+     * @return    XML_Feed_Parser_AtomElement
+     */
+    function getEntryById($id)
+    {
+        if (isset($this->idMappings[$id])) {
+            return $this->entries[$this->idMappings[$id]];
+        }
+
+        $entries = $this->xpath->query("//atom:entry[atom:id='$id']");
+
+        if ($entries->length > 0) {
+            $xmlBase = $entries->item(0)->baseURI;
+            $entry = new $this->itemElement($entries->item(0), $this, $xmlBase);
+            
+            if (in_array('evaluate', get_class_methods($this->xpath))) {
+                $offset = $this->xpath->evaluate("count(preceding-sibling::atom:entry)", $entries->item(0));
+                $this->entries[$offset] = $entry;
+            }
+
+            $this->idMappings[$id] = $entry;
+
+            return $entry;
+        }
+        
+    }
+
+    /**
+     * Retrieves data from a person construct.
+     *
+     * Get a person construct. We default to the 'name' element but allow
+     * access to any of the elements.
+     * 
+     * @param    string    $method    The name of the person construct we want
+     * @param    array     $arguments    An array which we hope gives a 'param'
+     * @return    string|false
+     */
+    protected function getPerson($method, $arguments)
+    {
+        $offset = empty($arguments[0]) ? 0 : $arguments[0];
+        $parameter = empty($arguments[1]['param']) ? 'name' : $arguments[1]['param'];
+        $section = $this->model->getElementsByTagName($method);
+        
+        if ($parameter == 'url') {
+            $parameter = 'uri';
+        }
+
+        if ($section->length <= $offset) {
+            return false;
+        }
+
+        $param = $section->item($offset)->getElementsByTagName($parameter);
+        if ($param->length == 0) {
+            return false;
+        }
+        return $param->item(0)->nodeValue;
+    }
+
+    /**
+     * Retrieves an element's content where that content is a text construct.
+     *
+     * Get a text construct. When calling this method, the two arguments
+     * allowed are 'offset' and 'attribute', so $parser->subtitle() would
+     * return the content of the element, while $parser->subtitle(false, 'type')
+     * would return the value of the type attribute.
+     *
+     * @todo    Clarify overlap with getContent()
+     * @param    string    $method    The name of the text construct we want
+     * @param    array     $arguments    An array which we hope gives a 'param'
+     * @return    string
+     */
+    protected function getText($method, $arguments)
+    {
+        $offset = empty($arguments[0]) ? 0: $arguments[0];
+        $attribute = empty($arguments[1]) ? false : $arguments[1];
+        $tags = $this->model->getElementsByTagName($method);
+
+        if ($tags->length <= $offset) {
+            return false;
+        }
+
+        $content = $tags->item($offset);
+
+        if (! $content->hasAttribute('type')) {
+            $content->setAttribute('type', 'text');
+        }
+        $type = $content->getAttribute('type');
+
+        if (! empty($attribute) and 
+            ! ($method == 'generator' and $attribute == 'name')) {
+            if ($content->hasAttribute($attribute)) {
+                return $content->getAttribute($attribute);
+            } else if ($attribute == 'href' and $content->hasAttribute('uri')) {
+                return $content->getAttribute('uri');
+            }
+            return false;
+        }
+        return $this->parseTextConstruct($content);
+    }
+    
+    /**
+     * Extract content appropriately from atom text constructs
+     *
+     * Because of different rules applied to the content element and other text
+     * constructs, they are deployed as separate functions, but they share quite
+     * a bit of processing. This method performs the core common process, which is
+     * to apply the rules for different mime types in order to extract the content.
+     *
+     * @param   DOMNode $content    the text construct node to be parsed
+     * @return String
+     * @author James Stewart
+     **/
+    protected function parseTextConstruct(DOMNode $content)
+    {
+        if ($content->hasAttribute('type')) {
+            $type = $content->getAttribute('type');
+        } else {
+            $type = 'text';
+        }
+
+        if (strpos($type, 'text/') === 0) {
+            $type = 'text';
+        }
+        switch ($type) {
+            case 'text':
+                return $content->nodeValue;
+                break;
+            case 'html':
+                return str_replace('&lt;', '<', $content->nodeValue);
+                break;
+            case 'xhtml':
+                $container = $content->getElementsByTagName('div');
+                if ($container->length == 0) {
+                    return false;
+                }
+                $contents = $container->item(0);
+                if ($contents->hasChildNodes()) {
+                    /* Iterate through, applying xml:base and store the result */
+                    $result = '';
+                    foreach ($contents->childNodes as $node) {
+                        $result .= $this->traverseNode($node);
+                    }
+                    return utf8_decode($result);
+                }
+                break;
+            case preg_match('@^[a-zA-Z]+/[a-zA-Z+]*xml@i', $type) > 0:
+                return $content;
+                break;
+            case 'application/octet-stream':
+            default:
+                return base64_decode(trim($content->nodeValue));
+                break;
+        }
+        return false;
+    }
+    /**
+     * Get a category from the entry.
+     *
+     * A feed or entry can have any number of categories. A category can have the
+     * attributes term, scheme and label.
+     * 
+     * @param    string    $method    The name of the text construct we want
+     * @param    array     $arguments    An array which we hope gives a 'param'
+     * @return    string
+     */
+    function getCategory($method, $arguments)
+    {
+        $offset = empty($arguments[0]) ? 0: $arguments[0];
+        $attribute = empty($arguments[1]) ? 'term' : $arguments[1];
+        $categories = $this->model->getElementsByTagName('category');
+        if ($categories->length <= $offset) {
+            $category = $categories->item($offset);
+            if ($category->hasAttribute($attribute)) {
+                return $category->getAttribute($attribute);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * This element must be present at least once with rel="feed". This element may be 
+     * present any number of further times so long as there is no clash. If no 'rel' is 
+     * present and we're asked for one, we follow the example of the Universal Feed
+     * Parser and presume 'alternate'.
+     *
+     * @param    int    $offset    the position of the link within the container
+     * @param    string    $attribute    the attribute name required
+     * @param    array     an array of attributes to search by
+     * @return    string    the value of the attribute
+     */
+    function getLink($offset = 0, $attribute = 'href', $params = false)
+    {
+        if (is_array($params) and !empty($params)) {
+            $terms = array();
+            $alt_predicate = '';
+            $other_predicate = '';
+
+            foreach ($params as $key => $value) {
+                if ($key == 'rel' && $value == 'alternate') {
+                    $alt_predicate = '[not(@rel) or @rel="alternate"]';
+                } else {
+                    $terms[] = "@$key='$value'";
+                }
+            }
+            if (!empty($terms)) {
+                $other_predicate = '[' . join(' and ', $terms) . ']';
+            }
+            $query =  $this->xpathPrefix . 'atom:link' . $alt_predicate . $other_predicate;
+            $links = $this->xpath->query($query);
+        } else {
+            $links = $this->model->getElementsByTagName('link');
+        }
+        if ($links->length > $offset) {
+            if ($links->item($offset)->hasAttribute($attribute)) {
+                $value = $links->item($offset)->getAttribute($attribute);
+                if ($attribute == 'href') {
+                    $value = $this->addBase($value, $links->item($offset));
+                }
+                return $value;
+            } else if ($attribute == 'rel') {
+                return 'alternate';
+            }
+        }
+        return false;
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/XML/Feed/Parser/AtomElement.php b/lib/XML/Feed/Parser/AtomElement.php
new file mode 100644 (file)
index 0000000..d8a39fe
--- /dev/null
@@ -0,0 +1,261 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * AtomElement class for XML_Feed_Parser package
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: AtomElement.php,v 1.19 2007/03/26 12:43:11 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/**
+ * This class provides support for atom entries. It will usually be called by
+ * XML_Feed_Parser_Atom with which it shares many methods.
+ *
+ * @author    James Stewart <james@jystewart.net>
+ * @version    Release: 1.0.2
+ * @package XML_Feed_Parser
+ */
+class XML_Feed_Parser_AtomElement extends XML_Feed_Parser_Atom
+{
+    /**
+     * This will be a reference to the parent object for when we want
+     * to use a 'fallback' rule 
+     * @var XML_Feed_Parser_Atom
+     */
+    protected $parent;
+
+    /**
+     * When performing XPath queries we will use this prefix 
+     * @var string
+     */
+    private $xpathPrefix = '';
+    
+    /**
+     * xml:base values inherited by the element 
+     * @var string
+     */
+    protected $xmlBase;
+
+    /**
+     * Here we provide a few mappings for those very special circumstances in
+     * which it makes sense to map back to the RSS2 spec or to manage other
+     * compatibilities (eg. with the Univeral Feed Parser). Key is the other version's
+     * name for the command, value is an array consisting of the equivalent in our atom 
+     * api and any attributes needed to make the mapping.
+     * @var array
+     */
+    protected $compatMap = array(
+        'guid' => array('id'),
+        'links' => array('link'),
+        'tags' => array('category'),
+        'contributors' => array('contributor'));
+        
+    /**
+     * Our specific element map 
+     * @var array
+     */
+    protected $map = array(
+        'author' => array('Person', 'fallback'),
+        'contributor' => array('Person'),
+        'id' => array('Text', 'fail'),
+        'published' => array('Date'),
+        'updated' => array('Date', 'fail'),
+        'title' => array('Text', 'fail'),
+        'rights' => array('Text', 'fallback'),
+        'summary' => array('Text'),
+        'content' => array('Content'),
+        'link' => array('Link'),
+        'enclosure' => array('Enclosure'),
+        'category' => array('Category'));
+
+    /**
+     * Store useful information for later.
+     *
+     * @param   DOMElement  $element - this item as a DOM element
+     * @param   XML_Feed_Parser_Atom    $parent - the feed of which this is a member
+     */
+    function __construct(DOMElement $element, $parent, $xmlBase = '')
+    {
+        $this->model = $element;
+        $this->parent = $parent;
+        $this->xmlBase = $xmlBase;
+        $this->xpathPrefix = "//atom:entry[atom:id='" . $this->id . "']/";
+        $this->xpath = $this->parent->xpath;
+    }
+
+    /**
+     * Provides access to specific aspects of the author data for an atom entry
+     *
+     * Author data at the entry level is more complex than at the feed level.
+     * If atom:author is not present for the entry we need to look for it in
+     * an atom:source child of the atom:entry. If it's not there either, then
+     * we look to the parent for data.
+     *
+     * @param   array
+     * @return  string
+     */
+    function getAuthor($arguments)
+    {
+        /* Find out which part of the author data we're looking for */
+        if (isset($arguments['param'])) {
+            $parameter = $arguments['param'];
+        } else {
+            $parameter = 'name';
+        }
+        
+        $test = $this->model->getElementsByTagName('author');
+        if ($test->length > 0) {
+            $item = $test->item(0);
+            return $item->getElementsByTagName($parameter)->item(0)->nodeValue;
+        }
+        
+        $source = $this->model->getElementsByTagName('source');
+        if ($source->length > 0) {
+            $test = $this->model->getElementsByTagName('author');
+            if ($test->length > 0) {
+                $item = $test->item(0);
+                return $item->getElementsByTagName($parameter)->item(0)->nodeValue;
+            }
+        }
+        return $this->parent->getAuthor($arguments);
+    }
+
+    /**
+     * Returns the content of the content element or info on a specific attribute
+     *
+     * This element may or may not be present. It cannot be present more than
+     * once. It may have a 'src' attribute, in which case there's no content
+     * If not present, then the entry must have link with rel="alternate".
+     * If there is content we return it, if not and there's a 'src' attribute
+     * we return the value of that instead. The method can take an 'attribute'
+     * argument, in which case we return the value of that attribute if present.
+     * eg. $item->content("type") will return the type of the content. It is
+     * recommended that all users check the type before getting the content to
+     * ensure that their script is capable of handling the type of returned data.
+     * (data carried in the content element can be either 'text', 'html', 'xhtml', 
+     * or any standard MIME type).
+     *
+     * @return  string|false
+     */
+    protected function getContent($method, $arguments = array())
+    {
+        $attribute = empty($arguments[0]) ? false : $arguments[0];
+        $tags = $this->model->getElementsByTagName('content');
+
+        if ($tags->length == 0) {
+            return false;
+        }
+
+        $content = $tags->item(0);
+
+        if (! $content->hasAttribute('type')) {
+            $content->setAttribute('type', 'text');
+        }
+        if (! empty($attribute)) {
+            return $content->getAttribute($attribute);
+        }
+
+        $type = $content->getAttribute('type');
+
+        if (! empty($attribute)) {
+            if ($content->hasAttribute($attribute))
+            {
+                return $content->getAttribute($attribute);
+            }
+            return false;
+        }
+
+        if ($content->hasAttribute('src')) {
+            return $content->getAttribute('src');
+        }
+
+        return $this->parseTextConstruct($content);
+     }
+
+    /**
+     * For compatibility, this method provides a mapping to access enclosures.
+     *
+     * The Atom spec doesn't provide for an enclosure element, but it is
+     * generally supported using the link element with rel='enclosure'.
+     *
+     * @param   string  $method - for compatibility with our __call usage
+     * @param   array   $arguments - for compatibility with our __call usage
+     * @return  array|false
+     */
+    function getEnclosure($method, $arguments = array())
+    {
+        $offset = isset($arguments[0]) ? $arguments[0] : 0;
+        $query = "//atom:entry[atom:id='" . $this->getText('id', false) . 
+            "']/atom:link[@rel='enclosure']";
+
+        $encs = $this->parent->xpath->query($query);
+        if ($encs->length > $offset) {
+            try {
+                if (! $encs->item($offset)->hasAttribute('href')) {
+                    return false;
+                }
+                $attrs = $encs->item($offset)->attributes;
+                $length = $encs->item($offset)->hasAttribute('length') ? 
+                    $encs->item($offset)->getAttribute('length') : false;
+                return array(
+                    'url' => $attrs->getNamedItem('href')->value,
+                    'type' => $attrs->getNamedItem('type')->value,
+                    'length' => $length);
+            } catch (Exception $e) {
+                return false;
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * Get details of this entry's source, if available/relevant
+     *
+     * Where an atom:entry is taken from another feed then the aggregator
+     * is supposed to include an atom:source element which replicates at least
+     * the atom:id, atom:title, and atom:updated metadata from the original
+     * feed. Atom:source therefore has a very similar structure to atom:feed
+     * and if we find it we will return it as an XML_Feed_Parser_Atom object.
+     *
+     * @return  XML_Feed_Parser_Atom|false
+     */
+    function getSource()
+    {
+        $test = $this->model->getElementsByTagName('source');
+        if ($test->length == 0) {
+            return false;
+        }
+        $source = new XML_Feed_Parser_Atom($test->item(0));
+    }
+
+    /**
+     * Get the entry as an XML string
+     *
+     * Return an XML serialization of the feed, should it be required. Most 
+     * users however, will already have a serialization that they used when 
+     * instantiating the object.
+     *
+     * @return    string    XML serialization of element
+     */    
+    function __toString()
+    {
+        $simple = simplexml_import_dom($this->model);
+        return $simple->asXML();
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/XML/Feed/Parser/Exception.php b/lib/XML/Feed/Parser/Exception.php
new file mode 100644 (file)
index 0000000..abc034c
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Keeps the exception class for XML_Feed_Parser.
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL
+ * @version    CVS: $Id: Exception.php,v 1.3 2005/11/07 01:52:35 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+/**
+ * We are extending PEAR_Exception
+ */
+require_once 'PEAR/Exception.php';
+
+/**
+ * XML_Feed_Parser_Exception is a simple extension of PEAR_Exception, existing
+ * to help with identification of the source of exceptions.
+ *
+ * @author  James Stewart <james@jystewart.net>
+ * @version Release: 1.0.2
+ * @package XML_Feed_Parser
+ */ 
+class XML_Feed_Parser_Exception extends PEAR_Exception
+{
+
+}
+
+?>
\ No newline at end of file
diff --git a/lib/XML/Feed/Parser/RSS09.php b/lib/XML/Feed/Parser/RSS09.php
new file mode 100644 (file)
index 0000000..7004e01
--- /dev/null
@@ -0,0 +1,214 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * RSS0.9 class for XML_Feed_Parser
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: RSS09.php,v 1.5 2006/07/26 21:18:46 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/**
+ * This class handles RSS0.9 feeds.
+ * 
+ * @author    James Stewart <james@jystewart.net>
+ * @version    Release: 1.0.2
+ * @package XML_Feed_Parser
+ * @todo    Find a Relax NG URI we can use
+ */
+class XML_Feed_Parser_RSS09 extends XML_Feed_Parser_Type
+{
+    /**
+     * The URI of the RelaxNG schema used to (optionally) validate the feed 
+     * @var string
+     */
+    private $relax = '';
+
+    /**
+     * We're likely to use XPath, so let's keep it global
+     * @var DOMXPath
+     */
+    protected $xpath;
+
+    /**
+     * The feed type we are parsing 
+     * @var string
+     */
+    public $version = 'RSS 0.9';
+
+    /**
+     * The class used to represent individual items 
+     * @var string
+     */
+    protected $itemClass = 'XML_Feed_Parser_RSS09Element';
+    
+    /**
+     * The element containing entries 
+     * @var string
+     */
+    protected $itemElement = 'item';
+
+    /**
+     * Here we map those elements we're not going to handle individually
+     * to the constructs they are. The optional second parameter in the array
+     * tells the parser whether to 'fall back' (not apt. at the feed level) or
+     * fail if the element is missing. If the parameter is not set, the function
+     * will simply return false and leave it to the client to decide what to do.
+     * @var array
+     */
+    protected $map = array(
+        'title' => array('Text'),
+        'link' => array('Text'),
+        'description' => array('Text'),
+        'image' => array('Image'),
+        'textinput' => array('TextInput'));
+
+    /**
+     * Here we map some elements to their atom equivalents. This is going to be
+     * quite tricky to pull off effectively (and some users' methods may vary)
+     * but is worth trying. The key is the atom version, the value is RSS2.
+     * @var array
+     */
+    protected $compatMap = array(
+        'title' => array('title'),
+        'link' => array('link'),
+        'subtitle' => array('description'));
+
+    /**
+     * We will be working with multiple namespaces and it is useful to 
+     * keep them together 
+     * @var array
+     */
+    protected $namespaces = array(
+        'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
+
+    /**
+     * Our constructor does nothing more than its parent.
+     * 
+     * @todo    RelaxNG validation
+     * @param    DOMDocument    $xml    A DOM object representing the feed
+     * @param    bool (optional) $string    Whether or not to validate this feed
+     */
+    function __construct(DOMDocument $model, $strict = false)
+    {
+        $this->model = $model;
+
+        $this->xpath = new DOMXPath($model);
+        foreach ($this->namespaces as $key => $value) {
+            $this->xpath->registerNamespace($key, $value);
+        }            
+        $this->numberEntries = $this->count('item');
+    }
+
+    /**
+     * Included for compatibility -- will not work with RSS 0.9
+     *
+     * This is not something that will work with RSS0.9 as it does not have
+     * clear restrictions on the global uniqueness of IDs.
+     *
+     * @param    string    $id    any valid ID.
+     * @return    false
+     */
+    function getEntryById($id)
+    {
+        return false;        
+    }
+
+    /**
+     * Get details of the image associated with the feed.
+     *
+     * @return  array|false an array simply containing the child elements
+     */
+    protected function getImage()
+    {
+        $images = $this->model->getElementsByTagName('image');
+        if ($images->length > 0) {
+            $image = $images->item(0);
+            $details = array();
+            if ($image->hasChildNodes()) {
+                $details = array(
+                    'title' => $image->getElementsByTagName('title')->item(0)->value,
+                    'link' => $image->getElementsByTagName('link')->item(0)->value,
+                    'url' => $image->getElementsByTagName('url')->item(0)->value);
+            } else {
+                $details = array('title' => false,
+                    'link' => false,
+                    'url' => $image->attributes->getNamedItem('resource')->nodeValue);
+            }
+            $details = array_merge($details, 
+                array('description' => false, 'height' => false, 'width' => false));
+            if (! empty($details)) {
+                return $details;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * The textinput element is little used, but in the interests of
+     * completeness we will support it.
+     *
+     * @return  array|false
+     */
+    protected function getTextInput()
+    {
+        $inputs = $this->model->getElementsByTagName('textinput');
+        if ($inputs->length > 0) {
+            $input = $inputs->item(0);
+            $results = array();
+            $results['title'] = isset(
+                $input->getElementsByTagName('title')->item(0)->value) ? 
+                $input->getElementsByTagName('title')->item(0)->value : null;
+            $results['description'] = isset(
+                $input->getElementsByTagName('description')->item(0)->value) ? 
+                $input->getElementsByTagName('description')->item(0)->value : null;
+            $results['name'] = isset(
+                $input->getElementsByTagName('name')->item(0)->value) ? 
+                $input->getElementsByTagName('name')->item(0)->value : null;
+            $results['link'] = isset(
+                   $input->getElementsByTagName('link')->item(0)->value) ? 
+                   $input->getElementsByTagName('link')->item(0)->value : null;
+            if (empty($results['link']) && 
+                $input->attributes->getNamedItem('resource')) {
+                $results['link'] = $input->attributes->getNamedItem('resource')->nodeValue;
+            }
+            if (! empty($results)) {
+                return $results;
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * Get details of a link from the feed.
+     *
+     * In RSS1 a link is a text element but in order to ensure that we resolve
+     * URLs properly we have a special function for them.
+     *
+     * @return  string
+     */
+    function getLink($offset = 0, $attribute = 'href', $params = false)
+    {
+        $links = $this->model->getElementsByTagName('link');
+        if ($links->length <= $offset) {
+            return false;
+        }
+        $link = $links->item($offset);
+        return $this->addBase($link->nodeValue, $link);
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/XML/Feed/Parser/RSS09Element.php b/lib/XML/Feed/Parser/RSS09Element.php
new file mode 100644 (file)
index 0000000..6ec8c8d
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * RSS0.9 Element class for XML_Feed_Parser
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: RSS09Element.php,v 1.4 2006/06/30 17:41:56 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/*
+ * This class provides support for RSS 0.9 entries. It will usually be called by
+ * XML_Feed_Parser_RSS09 with which it shares many methods.
+ *
+ * @author    James Stewart <james@jystewart.net>
+ * @version    Release: 1.0.2
+ * @package XML_Feed_Parser
+ */
+class XML_Feed_Parser_RSS09Element extends XML_Feed_Parser_RSS09
+{
+    /**
+     * This will be a reference to the parent object for when we want
+     * to use a 'fallback' rule 
+     * @var XML_Feed_Parser_RSS09
+     */
+    protected $parent;
+
+    /**
+     * Our specific element map 
+     * @var array
+     */
+    protected $map = array(
+        'title' => array('Text'),
+        'link' => array('Link'));
+
+    /**
+     * Store useful information for later.
+     *
+     * @param   DOMElement  $element - this item as a DOM element
+     * @param   XML_Feed_Parser_RSS1 $parent - the feed of which this is a member
+     */
+    function __construct(DOMElement $element, $parent, $xmlBase = '')
+    {
+        $this->model = $element;
+        $this->parent = $parent;
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/XML/Feed/Parser/RSS1.php b/lib/XML/Feed/Parser/RSS1.php
new file mode 100644 (file)
index 0000000..952f60e
--- /dev/null
@@ -0,0 +1,277 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * RSS1 class for XML_Feed_Parser
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: RSS1.php,v 1.10 2006/07/27 13:52:05 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/**
+ * This class handles RSS1.0 feeds.
+ * 
+ * @author    James Stewart <james@jystewart.net>
+ * @version    Release: 1.0.2
+ * @package XML_Feed_Parser
+ * @todo    Find a Relax NG URI we can use
+ */
+class XML_Feed_Parser_RSS1 extends XML_Feed_Parser_Type
+{
+    /**
+     * The URI of the RelaxNG schema used to (optionally) validate the feed 
+     * @var string
+     */
+    private $relax = 'rss10.rnc';
+
+    /**
+     * We're likely to use XPath, so let's keep it global
+     * @var DOMXPath
+     */
+    protected $xpath;
+
+    /**
+     * The feed type we are parsing 
+     * @var string
+     */
+    public $version = 'RSS 1.0';
+
+    /**
+     * The class used to represent individual items 
+     * @var string
+     */
+    protected $itemClass = 'XML_Feed_Parser_RSS1Element';
+    
+    /**
+     * The element containing entries 
+     * @var string
+     */
+    protected $itemElement = 'item';
+
+    /**
+     * Here we map those elements we're not going to handle individually
+     * to the constructs they are. The optional second parameter in the array
+     * tells the parser whether to 'fall back' (not apt. at the feed level) or
+     * fail if the element is missing. If the parameter is not set, the function
+     * will simply return false and leave it to the client to decide what to do.
+     * @var array
+     */
+    protected $map = array(
+        'title' => array('Text'),
+        'link' => array('Text'),
+        'description' => array('Text'),
+        'image' => array('Image'),
+        'textinput' => array('TextInput'),
+        'updatePeriod' => array('Text'),
+        'updateFrequency' => array('Text'),
+        'updateBase' => array('Date'),
+        'rights' => array('Text'), # dc:rights
+        'description' => array('Text'), # dc:description
+        'creator' => array('Text'), # dc:creator
+        'publisher' => array('Text'), # dc:publisher
+        'contributor' => array('Text'), # dc:contributor
+        'date' => array('Date') # dc:contributor
+        );
+
+    /**
+     * Here we map some elements to their atom equivalents. This is going to be
+     * quite tricky to pull off effectively (and some users' methods may vary)
+     * but is worth trying. The key is the atom version, the value is RSS2.
+     * @var array
+     */
+    protected $compatMap = array(
+        'title' => array('title'),
+        'link' => array('link'),
+        'subtitle' => array('description'),
+        'author' => array('creator'),
+        'updated' => array('date'));
+
+    /**
+     * We will be working with multiple namespaces and it is useful to 
+     * keep them together 
+     * @var array
+     */
+    protected $namespaces = array(
+        'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
+        'rss' => 'http://purl.org/rss/1.0/',
+        'dc' => 'http://purl.org/rss/1.0/modules/dc/',
+        'content' => 'http://purl.org/rss/1.0/modules/content/',
+        'sy' => 'http://web.resource.org/rss/1.0/modules/syndication/');
+
+    /**
+     * Our constructor does nothing more than its parent.
+     * 
+     * @param    DOMDocument    $xml    A DOM object representing the feed
+     * @param    bool (optional) $string    Whether or not to validate this feed
+     */
+    function __construct(DOMDocument $model, $strict = false)
+    {
+        $this->model = $model;
+        if ($strict) {
+            $validate = $this->model->relaxNGValidate(self::getSchemaDir . 
+                DIRECTORY_SEPARATOR . $this->relax);
+            if (! $validate) {
+                throw new XML_Feed_Parser_Exception('Failed required validation');
+            }
+        }
+
+        $this->xpath = new DOMXPath($model);
+        foreach ($this->namespaces as $key => $value) {
+            $this->xpath->registerNamespace($key, $value);
+        }
+        $this->numberEntries = $this->count('item');
+    }
+
+    /**
+     * Allows retrieval of an entry by ID where the rdf:about attribute is used
+     *
+     * This is not really something that will work with RSS1 as it does not have
+     * clear restrictions on the global uniqueness of IDs. We will employ the
+     * _very_ hit and miss method of selecting entries based on the rdf:about
+     * attribute. If DOMXPath::evaluate is available, we also use that to store 
+     * a reference to the entry in the array used by getEntryByOffset so that 
+     * method does not have to seek out the entry if it's requested that way.
+     *
+     * @param    string    $id    any valid ID.
+     * @return    XML_Feed_Parser_RSS1Element
+     */
+    function getEntryById($id)
+    {
+        if (isset($this->idMappings[$id])) {
+            return $this->entries[$this->idMappings[$id]];
+        }
+
+        $entries = $this->xpath->query("//rss:item[@rdf:about='$id']");
+        if ($entries->length > 0) {
+            $classname = $this->itemClass;
+            $entry = new $classname($entries->item(0), $this);
+            if (in_array('evaluate', get_class_methods($this->xpath))) {
+                $offset = $this->xpath->evaluate("count(preceding-sibling::rss:item)", $entries->item(0));
+                $this->entries[$offset] = $entry;
+            }
+            $this->idMappings[$id] = $entry;
+            return $entry;
+        }
+        return false;
+    }
+
+    /**
+     * Get details of the image associated with the feed.
+     *
+     * @return  array|false an array simply containing the child elements
+     */
+    protected function getImage()
+    {
+        $images = $this->model->getElementsByTagName('image');
+        if ($images->length > 0) {
+            $image = $images->item(0);
+            $details = array();
+            if ($image->hasChildNodes()) {
+                $details = array(
+                    'title' => $image->getElementsByTagName('title')->item(0)->value,
+                    'link' => $image->getElementsByTagName('link')->item(0)->value,
+                    'url' => $image->getElementsByTagName('url')->item(0)->value);
+            } else {
+                $details = array('title' => false,
+                    'link' => false,
+                    'url' => $image->attributes->getNamedItem('resource')->nodeValue);
+            }
+            $details = array_merge($details, array('description' => false, 'height' => false, 'width' => false));
+            if (! empty($details)) {
+                return $details;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * The textinput element is little used, but in the interests of
+     * completeness we will support it.
+     *
+     * @return  array|false
+     */
+    protected function getTextInput()
+    {
+        $inputs = $this->model->getElementsByTagName('textinput');
+        if ($inputs->length > 0) {
+            $input = $inputs->item(0);
+            $results = array();
+            $results['title'] = isset(
+                $input->getElementsByTagName('title')->item(0)->value) ? 
+                $input->getElementsByTagName('title')->item(0)->value : null;
+            $results['description'] = isset(
+                $input->getElementsByTagName('description')->item(0)->value) ? 
+                $input->getElementsByTagName('description')->item(0)->value : null;
+            $results['name'] = isset(
+                $input->getElementsByTagName('name')->item(0)->value) ? 
+                $input->getElementsByTagName('name')->item(0)->value : null;
+            $results['link'] = isset(
+                   $input->getElementsByTagName('link')->item(0)->value) ? 
+                   $input->getElementsByTagName('link')->item(0)->value : null;
+            if (empty($results['link']) and 
+                $input->attributes->getNamedItem('resource')) {
+                $results['link'] = 
+                    $input->attributes->getNamedItem('resource')->nodeValue;
+            }
+            if (! empty($results)) {
+                return $results;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Employs various techniques to identify the author
+     *
+     * Dublin Core provides the dc:creator, dc:contributor, and dc:publisher
+     * elements for defining authorship in RSS1. We will try each of those in
+     * turn in order to simulate the atom author element and will return it
+     * as text.
+     *
+     * @return  array|false
+     */
+    function getAuthor()
+    {
+        $options = array('creator', 'contributor', 'publisher');
+        foreach ($options as $element) {
+            $test = $this->model->getElementsByTagName($element);
+            if ($test->length > 0) {
+                return $test->item(0)->value;
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * Retrieve a link
+     * 
+     * In RSS1 a link is a text element but in order to ensure that we resolve
+     * URLs properly we have a special function for them.
+     *
+     * @return  string
+     */
+    function getLink($offset = 0, $attribute = 'href', $params = false)
+    {
+        $links = $this->model->getElementsByTagName('link');
+        if ($links->length <= $offset) {
+            return false;
+        }
+        $link = $links->item($offset);
+        return $this->addBase($link->nodeValue, $link);
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/XML/Feed/Parser/RSS11.php b/lib/XML/Feed/Parser/RSS11.php
new file mode 100644 (file)
index 0000000..74babc1
--- /dev/null
@@ -0,0 +1,276 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * RSS1.1 class for XML_Feed_Parser
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: RSS11.php,v 1.6 2006/07/27 13:52:05 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/**
+ * This class handles RSS1.1 feeds. RSS1.1 is documented at:
+ * http://inamidst.com/rss1.1/
+ * 
+ * @author    James Stewart <james@jystewart.net>
+ * @version    Release: 1.0.2
+ * @package XML_Feed_Parser
+ * @todo    Support for RDF:List
+ * @todo    Ensure xml:lang is accessible to users
+ */
+class XML_Feed_Parser_RSS11 extends XML_Feed_Parser_Type
+{
+    /**
+     * The URI of the RelaxNG schema used to (optionally) validate the feed 
+     * @var string
+     */
+    private $relax = 'rss11.rnc';
+
+    /**
+     * We're likely to use XPath, so let's keep it global
+     * @var DOMXPath
+     */
+    protected $xpath;
+
+    /**
+     * The feed type we are parsing 
+     * @var string
+     */
+    public $version = 'RSS 1.0';
+
+    /**
+     * The class used to represent individual items 
+     * @var string
+     */
+    protected $itemClass = 'XML_Feed_Parser_RSS1Element';
+    
+    /**
+     * The element containing entries 
+     * @var string
+     */
+    protected $itemElement = 'item';
+
+    /**
+     * Here we map those elements we're not going to handle individually
+     * to the constructs they are. The optional second parameter in the array
+     * tells the parser whether to 'fall back' (not apt. at the feed level) or
+     * fail if the element is missing. If the parameter is not set, the function
+     * will simply return false and leave it to the client to decide what to do.
+     * @var array
+     */
+    protected $map = array(
+        'title' => array('Text'),
+        'link' => array('Text'),
+        'description' => array('Text'),
+        'image' => array('Image'),
+        'updatePeriod' => array('Text'),
+        'updateFrequency' => array('Text'),
+        'updateBase' => array('Date'),
+        'rights' => array('Text'), # dc:rights
+        'description' => array('Text'), # dc:description
+        'creator' => array('Text'), # dc:creator
+        'publisher' => array('Text'), # dc:publisher
+        'contributor' => array('Text'), # dc:contributor
+        'date' => array('Date') # dc:contributor
+        );
+
+    /**
+     * Here we map some elements to their atom equivalents. This is going to be
+     * quite tricky to pull off effectively (and some users' methods may vary)
+     * but is worth trying. The key is the atom version, the value is RSS2.
+     * @var array
+     */
+    protected $compatMap = array(
+        'title' => array('title'),
+        'link' => array('link'),
+        'subtitle' => array('description'),
+        'author' => array('creator'),
+        'updated' => array('date'));
+
+    /**
+     * We will be working with multiple namespaces and it is useful to 
+     * keep them together. We will retain support for some common RSS1.0 modules
+     * @var array
+     */
+    protected $namespaces = array(
+        'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
+        'rss' => 'http://purl.org/net/rss1.1#',
+        'dc' => 'http://purl.org/rss/1.0/modules/dc/',
+        'content' => 'http://purl.org/rss/1.0/modules/content/',
+        'sy' => 'http://web.resource.org/rss/1.0/modules/syndication/');
+
+    /**
+     * Our constructor does nothing more than its parent.
+     * 
+     * @param    DOMDocument    $xml    A DOM object representing the feed
+     * @param    bool (optional) $string    Whether or not to validate this feed
+     */
+    function __construct(DOMDocument $model, $strict = false)
+    {
+        $this->model = $model;
+
+        if ($strict) {
+            $validate = $this->model->relaxNGValidate(self::getSchemaDir . 
+                DIRECTORY_SEPARATOR . $this->relax);
+            if (! $validate) {
+                throw new XML_Feed_Parser_Exception('Failed required validation');
+            }
+        }
+
+        $this->xpath = new DOMXPath($model);
+        foreach ($this->namespaces as $key => $value) {
+            $this->xpath->registerNamespace($key, $value);
+        }            
+        $this->numberEntries = $this->count('item');
+    }
+
+    /**
+     * Attempts to identify an element by ID given by the rdf:about attribute
+     *
+     * This is not really something that will work with RSS1.1 as it does not have
+     * clear restrictions on the global uniqueness of IDs. We will employ the
+     * _very_ hit and miss method of selecting entries based on the rdf:about
+     * attribute. Please note that this is even more hit and miss with RSS1.1 than
+     * with RSS1.0 since RSS1.1 does not require the rdf:about attribute for items.
+     *
+     * @param    string    $id    any valid ID.
+     * @return    XML_Feed_Parser_RSS1Element
+     */
+    function getEntryById($id)
+    {
+        if (isset($this->idMappings[$id])) {
+            return $this->entries[$this->idMappings[$id]];
+        }
+
+        $entries = $this->xpath->query("//rss:item[@rdf:about='$id']");
+        if ($entries->length > 0) {
+            $classname = $this->itemClass;
+            $entry = new $classname($entries->item(0), $this);
+            return $entry;
+        }
+        return false;
+    }
+
+    /**
+     * Get details of the image associated with the feed.
+     *
+     * @return  array|false an array simply containing the child elements
+     */
+    protected function getImage()
+    {
+        $images = $this->model->getElementsByTagName('image');
+        if ($images->length > 0) {
+            $image = $images->item(0);
+            $details = array();
+            if ($image->hasChildNodes()) {
+                $details = array(
+                    'title' => $image->getElementsByTagName('title')->item(0)->value,
+                    'url' => $image->getElementsByTagName('url')->item(0)->value);
+                if ($image->getElementsByTagName('link')->length > 0) {
+                    $details['link'] = 
+                        $image->getElementsByTagName('link')->item(0)->value;
+                }
+            } else {
+                $details = array('title' => false,
+                    'link' => false,
+                    'url' => $image->attributes->getNamedItem('resource')->nodeValue);
+            }
+            $details = array_merge($details, 
+                array('description' => false, 'height' => false, 'width' => false));
+            if (! empty($details)) {
+                return $details;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * The textinput element is little used, but in the interests of
+     * completeness we will support it.
+     *
+     * @return  array|false
+     */
+    protected function getTextInput()
+    {
+        $inputs = $this->model->getElementsByTagName('textinput');
+        if ($inputs->length > 0) {
+            $input = $inputs->item(0);
+            $results = array();
+            $results['title'] = isset(
+                $input->getElementsByTagName('title')->item(0)->value) ? 
+                $input->getElementsByTagName('title')->item(0)->value : null;
+            $results['description'] = isset(
+                $input->getElementsByTagName('description')->item(0)->value) ? 
+                $input->getElementsByTagName('description')->item(0)->value : null;
+            $results['name'] = isset(
+                $input->getElementsByTagName('name')->item(0)->value) ? 
+                $input->getElementsByTagName('name')->item(0)->value : null;
+            $results['link'] = isset(
+                   $input->getElementsByTagName('link')->item(0)->value) ? 
+                   $input->getElementsByTagName('link')->item(0)->value : null;
+            if (empty($results['link']) and 
+                $input->attributes->getNamedItem('resource')) {
+                $results['link'] = $input->attributes->getNamedItem('resource')->nodeValue;
+            }
+            if (! empty($results)) {
+                return $results;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Attempts to discern authorship
+     *
+     * Dublin Core provides the dc:creator, dc:contributor, and dc:publisher
+     * elements for defining authorship in RSS1. We will try each of those in
+     * turn in order to simulate the atom author element and will return it
+     * as text.
+     *
+     * @return  array|false
+     */
+    function getAuthor()
+    {
+        $options = array('creator', 'contributor', 'publisher');
+        foreach ($options as $element) {
+            $test = $this->model->getElementsByTagName($element);
+            if ($test->length > 0) {
+                return $test->item(0)->value;
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * Retrieve a link
+     *
+     * In RSS1 a link is a text element but in order to ensure that we resolve
+     * URLs properly we have a special function for them.
+     *
+     * @return  string
+     */
+    function getLink($offset = 0, $attribute = 'href', $params = false)
+    {
+        $links = $this->model->getElementsByTagName('link');
+        if ($links->length <= $offset) {
+            return false;
+        }
+        $link = $links->item($offset);
+        return $this->addBase($link->nodeValue, $link);
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/XML/Feed/Parser/RSS11Element.php b/lib/XML/Feed/Parser/RSS11Element.php
new file mode 100644 (file)
index 0000000..9de6417
--- /dev/null
@@ -0,0 +1,151 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * RSS1 Element class for XML_Feed_Parser
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: RSS11Element.php,v 1.4 2006/06/30 17:41:56 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/*
+ * This class provides support for RSS 1.1 entries. It will usually be called by
+ * XML_Feed_Parser_RSS11 with which it shares many methods.
+ *
+ * @author    James Stewart <james@jystewart.net>
+ * @version    Release: 1.0.2
+ * @package XML_Feed_Parser
+ */
+class XML_Feed_Parser_RSS11Element extends XML_Feed_Parser_RSS11
+{
+    /**
+     * This will be a reference to the parent object for when we want
+     * to use a 'fallback' rule 
+     * @var XML_Feed_Parser_RSS1
+     */
+    protected $parent;
+
+    /**
+     * Our specific element map 
+     * @var array
+     */
+    protected $map = array(
+        'id' => array('Id'),
+        'title' => array('Text'),
+        'link' => array('Link'),
+        'description' => array('Text'), # or dc:description
+        'category' => array('Category'),
+        'rights' => array('Text'), # dc:rights
+        'creator' => array('Text'), # dc:creator
+        'publisher' => array('Text'), # dc:publisher
+        'contributor' => array('Text'), # dc:contributor
+        'date' => array('Date'), # dc:date
+        'content' => array('Content')
+        );
+
+    /**
+     * Here we map some elements to their atom equivalents. This is going to be
+     * quite tricky to pull off effectively (and some users' methods may vary)
+     * but is worth trying. The key is the atom version, the value is RSS1.
+     * @var array
+     */
+    protected $compatMap = array(
+        'content' => array('content'),
+        'updated' => array('lastBuildDate'),
+        'published' => array('pubdate'),
+        'subtitle' => array('description'),
+        'updated' => array('date'),
+        'author' => array('creator'),
+        'contributor' => array('contributor')
+    );
+
+    /**
+     * Store useful information for later.
+     *
+     * @param   DOMElement  $element - this item as a DOM element
+     * @param   XML_Feed_Parser_RSS1 $parent - the feed of which this is a member
+     */
+    function __construct(DOMElement $element, $parent, $xmlBase = '')
+    {
+        $this->model = $element;
+        $this->parent = $parent;
+    }
+
+    /**
+     * If an rdf:about attribute is specified, return that as an ID
+     *
+     * There is no established way of showing an ID for an RSS1 entry. We will 
+     * simulate it using the rdf:about attribute of the entry element. This cannot
+     * be relied upon for unique IDs but may prove useful.
+     *
+     * @return  string|false
+     */
+    function getId()
+    {
+        if ($this->model->attributes->getNamedItem('about')) {
+            return $this->model->attributes->getNamedItem('about')->nodeValue;
+        }
+        return false;
+    }
+
+    /**
+     * Return the entry's content
+     *
+     * The official way to include full content in an RSS1 entry is to use
+     * the content module's element 'encoded'. Often, however, the 'description'
+     * element is used instead. We will offer that as a fallback.
+     *
+     * @return  string|false
+     */
+    function getContent()
+    {
+        $options = array('encoded', 'description');
+        foreach ($options as $element) {
+            $test = $this->model->getElementsByTagName($element);
+            if ($test->length == 0) {
+                continue;
+            }
+            if ($test->item(0)->hasChildNodes()) {
+                $value = '';
+                foreach ($test->item(0)->childNodes as $child) {
+                    if ($child instanceof DOMText) {
+                        $value .= $child->nodeValue;
+                    } else {
+                        $simple = simplexml_import_dom($child);
+                        $value .= $simple->asXML();
+                    }
+                }
+                return $value;
+            } else if ($test->length > 0) {
+                return $test->item(0)->nodeValue;
+            }
+        }
+        return false;
+    }
+    
+    /**
+     * How RSS1.1 should support for enclosures is not clear. For now we will return
+     * false.
+     *
+     * @return  false
+     */
+    function getEnclosure()
+    {
+        return false;
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/XML/Feed/Parser/RSS1Element.php b/lib/XML/Feed/Parser/RSS1Element.php
new file mode 100644 (file)
index 0000000..f43ae46
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * RSS1 Element class for XML_Feed_Parser
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: RSS1Element.php,v 1.6 2006/06/30 17:41:56 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/*
+ * This class provides support for RSS 1.0 entries. It will usually be called by
+ * XML_Feed_Parser_RSS1 with which it shares many methods.
+ *
+ * @author    James Stewart <james@jystewart.net>
+ * @version    Release: 1.0.2
+ * @package XML_Feed_Parser
+ */
+class XML_Feed_Parser_RSS1Element extends XML_Feed_Parser_RSS1
+{
+    /**
+     * This will be a reference to the parent object for when we want
+     * to use a 'fallback' rule 
+     * @var XML_Feed_Parser_RSS1
+     */
+    protected $parent;
+
+    /**
+     * Our specific element map 
+     * @var array
+     */
+    protected $map = array(
+        'id' => array('Id'),
+        'title' => array('Text'),
+        'link' => array('Link'),
+        'description' => array('Text'), # or dc:description
+        'category' => array('Category'),
+        'rights' => array('Text'), # dc:rights
+        'creator' => array('Text'), # dc:creator
+        'publisher' => array('Text'), # dc:publisher
+        'contributor' => array('Text'), # dc:contributor
+        'date' => array('Date'), # dc:date
+        'content' => array('Content')
+        );
+
+    /**
+     * Here we map some elements to their atom equivalents. This is going to be
+     * quite tricky to pull off effectively (and some users' methods may vary)
+     * but is worth trying. The key is the atom version, the value is RSS1.
+     * @var array
+     */
+    protected $compatMap = array(
+        'content' => array('content'),
+        'updated' => array('lastBuildDate'),
+        'published' => array('pubdate'),
+        'subtitle' => array('description'),
+        'updated' => array('date'),
+        'author' => array('creator'),
+        'contributor' => array('contributor')
+    );
+
+    /**
+     * Store useful information for later.
+     *
+     * @param   DOMElement  $element - this item as a DOM element
+     * @param   XML_Feed_Parser_RSS1 $parent - the feed of which this is a member
+     */
+    function __construct(DOMElement $element, $parent, $xmlBase = '')
+    {
+        $this->model = $element;
+        $this->parent = $parent;
+    }
+
+    /**
+     * If an rdf:about attribute is specified, return it as an ID
+     *
+     * There is no established way of showing an ID for an RSS1 entry. We will 
+     * simulate it using the rdf:about attribute of the entry element. This cannot
+     * be relied upon for unique IDs but may prove useful.
+     *
+     * @return  string|false
+     */
+    function getId()
+    {
+        if ($this->model->attributes->getNamedItem('about')) {
+            return $this->model->attributes->getNamedItem('about')->nodeValue;
+        }
+        return false;
+    }
+
+    /**
+     * How RSS1 should support for enclosures is not clear. For now we will return
+     * false.
+     *
+     * @return  false
+     */
+    function getEnclosure()
+    {
+        return false;
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/XML/Feed/Parser/RSS2.php b/lib/XML/Feed/Parser/RSS2.php
new file mode 100644 (file)
index 0000000..c5d79d1
--- /dev/null
@@ -0,0 +1,334 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Class representing feed-level data for an RSS2 feed
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: RSS2.php,v 1.11 2006/07/27 13:52:05 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/**
+ * This class handles RSS2 feeds.
+ * 
+ * @author    James Stewart <james@jystewart.net>
+ * @version    Release: 1.0.2
+ * @package XML_Feed_Parser
+ */
+class XML_Feed_Parser_RSS2 extends XML_Feed_Parser_Type
+{
+    /**
+     * The URI of the RelaxNG schema used to (optionally) validate the feed
+     * @var string
+     */
+    private $relax = 'rss20.rnc';
+
+    /**
+     * We're likely to use XPath, so let's keep it global
+     * @var DOMXPath
+     */
+    protected $xpath;
+
+    /**
+     * The feed type we are parsing
+     * @var string
+     */
+    public $version = 'RSS 2.0';
+
+    /**
+     * The class used to represent individual items
+     * @var string
+     */     
+    protected $itemClass = 'XML_Feed_Parser_RSS2Element';
+    
+    /**
+     * The element containing entries 
+     * @var string
+     */
+    protected $itemElement = 'item';
+
+    /**
+     * Here we map those elements we're not going to handle individually
+     * to the constructs they are. The optional second parameter in the array
+     * tells the parser whether to 'fall back' (not apt. at the feed level) or
+     * fail if the element is missing. If the parameter is not set, the function
+     * will simply return false and leave it to the client to decide what to do.
+     * @var array
+     */
+    protected $map = array(
+        'ttl' => array('Text'),
+        'pubDate' => array('Date'),
+        'lastBuildDate' => array('Date'),
+        'title' => array('Text'),
+        'link' => array('Link'),
+        'description' => array('Text'),
+        'language' => array('Text'),
+        'copyright' => array('Text'),
+        'managingEditor' => array('Text'),
+        'webMaster' => array('Text'),
+        'category' => array('Text'),
+        'generator' => array('Text'),
+        'docs' => array('Text'),
+        'ttl' => array('Text'),
+        'image' => array('Image'),
+        'skipDays' => array('skipDays'),
+        'skipHours' => array('skipHours'));
+
+    /**
+     * Here we map some elements to their atom equivalents. This is going to be
+     * quite tricky to pull off effectively (and some users' methods may vary)
+     * but is worth trying. The key is the atom version, the value is RSS2.
+     * @var array
+     */
+    protected $compatMap = array(
+        'title' => array('title'),
+        'rights' => array('copyright'),
+        'updated' => array('lastBuildDate'),
+        'subtitle' => array('description'),
+        'date' => array('pubDate'),
+        'author' => array('managingEditor'));
+
+    protected $namespaces = array(
+        'dc' => 'http://purl.org/rss/1.0/modules/dc/',
+        'content' => 'http://purl.org/rss/1.0/modules/content/');
+
+    /**
+     * Our constructor does nothing more than its parent.
+     * 
+     * @param    DOMDocument    $xml    A DOM object representing the feed
+     * @param    bool (optional) $string    Whether or not to validate this feed
+     */
+    function __construct(DOMDocument $model, $strict = false)
+    {
+        $this->model = $model;
+
+        if ($strict) {
+            if (! $this->model->relaxNGValidate($this->relax)) {
+                throw new XML_Feed_Parser_Exception('Failed required validation');
+            }
+        }
+
+        $this->xpath = new DOMXPath($this->model);
+        foreach ($this->namespaces as $key => $value) {
+            $this->xpath->registerNamespace($key, $value);
+        }
+        $this->numberEntries = $this->count('item');
+    }
+
+    /**
+     * Retrieves an entry by ID, if the ID is specified with the guid element
+     *
+     * This is not really something that will work with RSS2 as it does not have
+     * clear restrictions on the global uniqueness of IDs. But we can emulate
+     * it by allowing access based on the 'guid' element. If DOMXPath::evaluate
+     * is available, we also use that to store a reference to the entry in the array
+     * used by getEntryByOffset so that method does not have to seek out the entry
+     * if it's requested that way.
+     *
+     * @param    string    $id    any valid ID.
+     * @return    XML_Feed_Parser_RSS2Element
+     */
+    function getEntryById($id)
+    {
+        if (isset($this->idMappings[$id])) {
+            return $this->entries[$this->idMappings[$id]];
+        }
+
+        $entries = $this->xpath->query("//item[guid='$id']");
+        if ($entries->length > 0) {
+            $entry = new $this->itemElement($entries->item(0), $this);
+            if (in_array('evaluate', get_class_methods($this->xpath))) {
+                $offset = $this->xpath->evaluate("count(preceding-sibling::item)", $entries->item(0));
+                $this->entries[$offset] = $entry;
+            }
+            $this->idMappings[$id] = $entry;
+            return $entry;
+        }        
+    }
+
+    /**
+     * Get a category from the element
+     *
+     * The category element is a simple text construct which can occur any number
+     * of times. We allow access by offset or access to an array of results.
+     *
+     * @param    string    $call    for compatibility with our overloading
+     * @param   array $arguments - arg 0 is the offset, arg 1 is whether to return as array
+     * @return  string|array|false
+     */
+    function getCategory($call, $arguments = array())
+    {
+        $categories = $this->model->getElementsByTagName('category');
+        $offset = empty($arguments[0]) ? 0 : $arguments[0];
+        $array = empty($arguments[1]) ? false : true;
+        if ($categories->length <= $offset) {
+            return false;
+        }
+        if ($array) {
+            $list = array();
+            foreach ($categories as $category) {
+                array_push($list, $category->nodeValue);
+            }
+            return $list;
+        }
+        return $categories->item($offset)->nodeValue;
+    }
+
+    /**
+     * Get details of the image associated with the feed.
+     *
+     * @return  array|false an array simply containing the child elements
+     */
+    protected function getImage()
+    {
+        $images = $this->model->getElementsByTagName('image');
+        if ($images->length > 0) {
+            $image = $images->item(0);
+            $desc = $image->getElementsByTagName('description');
+            $description = $desc->length ? $desc->item(0)->nodeValue : false;
+            $heigh = $image->getElementsByTagName('height'); 
+            $height = $heigh->length ? $heigh->item(0)->nodeValue : false;
+            $widt = $image->getElementsByTagName('width'); 
+            $width = $widt->length ? $widt->item(0)->nodeValue : false;
+            return array(
+                'title' => $image->getElementsByTagName('title')->item(0)->nodeValue,
+                'link' => $image->getElementsByTagName('link')->item(0)->nodeValue,
+                'url' => $image->getElementsByTagName('url')->item(0)->nodeValue,
+                'description' => $description,
+                'height' => $height,
+                'width' => $width);
+        }
+        return false;
+    }
+
+    /**
+     * The textinput element is little used, but in the interests of
+     * completeness...
+     *
+     * @return  array|false
+     */
+    function getTextInput()
+    {
+        $inputs = $this->model->getElementsByTagName('input');
+        if ($inputs->length > 0) {
+            $input = $inputs->item(0);
+            return array(
+                'title' => $input->getElementsByTagName('title')->item(0)->value,
+                'description' => 
+                    $input->getElementsByTagName('description')->item(0)->value,
+                'name' => $input->getElementsByTagName('name')->item(0)->value,
+                'link' => $input->getElementsByTagName('link')->item(0)->value);
+        }
+        return false;
+    }
+
+    /**
+     * Utility function for getSkipDays and getSkipHours
+     *
+     * This is a general function used by both getSkipDays and getSkipHours. It simply
+     * returns an array of the values of the children of the appropriate tag.
+     *
+     * @param   string      $tagName    The tag name (getSkipDays or getSkipHours)
+     * @return  array|false
+     */
+    protected function getSkips($tagName)
+    {
+        $hours = $this->model->getElementsByTagName($tagName);
+        if ($hours->length == 0) {
+            return false;
+        }
+        $skipHours = array();
+        foreach($hours->item(0)->childNodes as $hour) {
+            if ($hour instanceof DOMElement) {
+                array_push($skipHours, $hour->nodeValue);
+            }
+        }
+        return $skipHours;
+    }
+
+    /**
+     * Retrieve skipHours data
+     *
+     * The skiphours element provides a list of hours on which this feed should
+     * not be checked. We return an array of those hours (integers, 24 hour clock)
+     *
+     * @return  array
+     */    
+    function getSkipHours()
+    {
+        return $this->getSkips('skipHours');
+    }
+
+    /**
+     * Retrieve skipDays data
+     *
+     * The skipdays element provides a list of days on which this feed should
+     * not be checked. We return an array of those days.
+     *
+     * @return  array
+     */
+    function getSkipDays()
+    {
+        return $this->getSkips('skipDays');
+    }
+
+    /**
+     * Return content of the little-used 'cloud' element
+     *
+     * The cloud element is rarely used. It is designed to provide some details
+     * of a location to update the feed.
+     *
+     * @return  array   an array of the attributes of the element
+     */
+    function getCloud()
+    {
+        $cloud = $this->model->getElementsByTagName('cloud');
+        if ($cloud->length == 0) {
+            return false;
+        }
+        $cloudData = array();
+        foreach ($cloud->item(0)->attributes as $attribute) {
+            $cloudData[$attribute->name] = $attribute->value;
+        }
+        return $cloudData;
+    }
+    
+    /**
+     * Get link URL
+     *
+     * In RSS2 a link is a text element but in order to ensure that we resolve
+     * URLs properly we have a special function for them. We maintain the 
+     * parameter used by the atom getLink method, though we only use the offset
+     * parameter.
+     *
+     * @param   int     $offset The position of the link within the feed. Starts from 0
+     * @param   string  $attribute  The attribute of the link element required
+     * @param   array   $params An array of other parameters. Not used.
+     * @return  string
+     */
+    function getLink($offset, $attribute = 'href', $params = array())
+    {
+        $links = $this->model->getElementsByTagName('link');
+
+        if ($links->length <= $offset) {
+            return false;
+        }
+        $link = $links->item($offset);
+        return $this->addBase($link->nodeValue, $link);
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/XML/Feed/Parser/RSS2Element.php b/lib/XML/Feed/Parser/RSS2Element.php
new file mode 100644 (file)
index 0000000..156e9bc
--- /dev/null
@@ -0,0 +1,171 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Class representing entries in an RSS2 feed.
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: RSS2Element.php,v 1.11 2006/07/26 21:18:47 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/**
+ * This class provides support for RSS 2.0 entries. It will usually be 
+ * called by XML_Feed_Parser_RSS2 with which it shares many methods.
+ *
+ * @author    James Stewart <james@jystewart.net>
+ * @version    Release: 1.0.2
+ * @package XML_Feed_Parser
+ */
+class XML_Feed_Parser_RSS2Element extends XML_Feed_Parser_RSS2
+{
+    /**
+     * This will be a reference to the parent object for when we want
+     * to use a 'fallback' rule
+     * @var XML_Feed_Parser_RSS2
+     */
+    protected $parent;
+
+    /**
+     * Our specific element map 
+     * @var array
+     */
+    protected $map = array(
+        'title' => array('Text'),
+        'guid' => array('Guid'),
+        'description' => array('Text'),
+        'author' => array('Text'),
+        'comments' => array('Text'),
+        'enclosure' => array('Enclosure'),
+        'pubDate' => array('Date'),
+        'source' => array('Source'),
+        'link' => array('Text'),
+        'content' => array('Content'));
+
+    /**
+     * Here we map some elements to their atom equivalents. This is going to be
+     * quite tricky to pull off effectively (and some users' methods may vary)
+     * but is worth trying. The key is the atom version, the value is RSS2.
+     * @var array
+     */
+    protected $compatMap = array(
+        'id' => array('guid'),
+        'updated' => array('lastBuildDate'),
+        'published' => array('pubdate'),
+        'guidislink' => array('guid', 'ispermalink'),
+        'summary' => array('description'));
+
+    /**
+     * Store useful information for later.
+     *
+     * @param   DOMElement  $element - this item as a DOM element
+     * @param   XML_Feed_Parser_RSS2    $parent - the feed of which this is a member
+     */
+    function __construct(DOMElement $element, $parent, $xmlBase = '')
+    {
+        $this->model = $element;
+        $this->parent = $parent;
+    }
+
+    /**
+     * Get the value of the guid element, if specified
+     *
+     * guid is the closest RSS2 has to atom's ID. It is usually but not always a
+     * URI. The one attribute that RSS2 can posess is 'ispermalink' which specifies
+     * whether the guid is itself dereferencable. Use of guid is not obligatory,
+     * but is advisable. To get the guid you would call $item->id() (for atom
+     * compatibility) or $item->guid(). To check if this guid is a permalink call
+     * $item->guid("ispermalink").
+     *
+     * @param   string  $method - the method name being called
+     * @param   array   $params - parameters required
+     * @return  string  the guid or value of ispermalink
+     */
+    protected function getGuid($method, $params)
+    {
+        $attribute = (isset($params[0]) and $params[0] == 'ispermalink') ? 
+            true : false;
+        $tag = $this->model->getElementsByTagName('guid');
+        if ($tag->length > 0) {
+            if ($attribute) {
+                if ($tag->hasAttribute("ispermalink")) {
+                    return $tag->getAttribute("ispermalink");
+                }
+            }
+            return $tag->item(0)->nodeValue;
+        }
+        return false;
+    }
+
+    /**
+     * Access details of file enclosures
+     *
+     * The RSS2 spec is ambiguous as to whether an enclosure element must be
+     * unique in a given entry. For now we will assume it needn't, and allow
+     * for an offset.
+     *
+     * @param   string $method - the method being called
+     * @param   array   $parameters - we expect the first of these to be our offset
+     * @return  array|false
+     */
+    protected function getEnclosure($method, $parameters)
+    {
+        $encs = $this->model->getElementsByTagName('enclosure');
+        $offset = isset($parameters[0]) ? $parameters[0] : 0;
+        if ($encs->length > $offset) {
+            try {
+                if (! $encs->item($offset)->hasAttribute('url')) {
+                    return false;
+                }
+                $attrs = $encs->item($offset)->attributes;
+                return array(
+                    'url' => $attrs->getNamedItem('url')->value,
+                    'length' => $attrs->getNamedItem('length')->value,
+                    'type' => $attrs->getNamedItem('type')->value);
+            } catch (Exception $e) {
+                return false;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Get the entry source if specified
+     *
+     * source is an optional sub-element of item. Like atom:source it tells
+     * us about where the entry came from (eg. if it's been copied from another
+     * feed). It is not a rich source of metadata in the same way as atom:source
+     * and while it would be good to maintain compatibility by returning an
+     * XML_Feed_Parser_RSS2 element, it makes a lot more sense to return an array.
+     *
+     * @return array|false
+     */
+    protected function getSource()
+    {
+        $get = $this->model->getElementsByTagName('source');
+        if ($get->length) {
+            $source = $get->item(0);
+            $array = array(
+                'content' => $source->nodeValue);
+            foreach ($source->attributes as $attribute) {
+                $array[$attribute->name] = $attribute->value;
+            }
+            return $array;
+        }
+        return false;
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/XML/Feed/Parser/Type.php b/lib/XML/Feed/Parser/Type.php
new file mode 100644 (file)
index 0000000..9ccf48b
--- /dev/null
@@ -0,0 +1,441 @@
+<?php
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * Abstract class providing common methods for XML_Feed_Parser feeds.
+ *
+ * PHP versions 5
+ *
+ * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * that is available through the world-wide-web at the following URI:
+ * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
+ * the PHP License and are unable to obtain it through the web, please
+ * send a note to license@php.net so we can mail you a copy immediately.
+ *
+ * @category   XML
+ * @package    XML_Feed_Parser
+ * @author     James Stewart <james@jystewart.net>
+ * @copyright  2005 James Stewart <james@jystewart.net>
+ * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
+ * @version    CVS: $Id: Type.php,v 1.22 2006/08/15 13:02:36 jystewart Exp $
+ * @link       http://pear.php.net/package/XML_Feed_Parser/
+ */
+
+/**
+ * This abstract class provides some general methods that are likely to be
+ * implemented exactly the same way for all feed types.
+ *
+ * @package XML_Feed_Parser
+ * @author  James Stewart <james@jystewart.net>
+ * @version Release: 1.0.2
+ */
+abstract class XML_Feed_Parser_Type
+{
+    /**
+     * Where we store our DOM object for this feed 
+     * @var DOMDocument
+     */
+    public $model;
+
+    /**
+     * For iteration we'll want a count of the number of entries 
+     * @var int
+     */
+    public $numberEntries;
+
+    /**
+     * Where we store our entry objects once instantiated 
+     * @var array
+     */
+    public $entries = array();
+
+    /**
+     * Proxy to allow use of element names as method names
+     *
+     * We are not going to provide methods for every entry type so this
+     * function will allow for a lot of mapping. We rely pretty heavily
+     * on this to handle our mappings between other feed types and atom.
+     *
+     * @param   string  $call - the method attempted
+     * @param   array   $arguments - arguments to that method
+     * @return  mixed
+     */
+    function __call($call, $arguments = array())
+    {
+        if (! is_array($arguments)) {
+            $arguments = array();
+        }
+
+        if (isset($this->compatMap[$call])) {
+            $tempMap = $this->compatMap;
+            $tempcall = array_pop($tempMap[$call]);
+            if (! empty($tempMap)) {
+                $arguments = array_merge($arguments, $tempMap[$call]);
+            }
+            $call = $tempcall;
+        }
+
+        /* To be helpful, we allow a case-insensitive search for this method */
+        if (! isset($this->map[$call])) {
+            foreach (array_keys($this->map) as $key) {
+                if (strtoupper($key) == strtoupper($call)) {
+                    $call = $key;
+                    break;
+                }
+            }
+        }
+
+        if (empty($this->map[$call])) {
+            return false;
+        }
+
+        $method = 'get' . $this->map[$call][0];
+        if ($method == 'getLink') {
+            $offset = empty($arguments[0]) ? 0 : $arguments[0];
+            $attribute = empty($arguments[1]) ? 'href' : $arguments[1];
+            $params = isset($arguments[2]) ? $arguments[2] : array();
+            return $this->getLink($offset, $attribute, $params);
+        }
+        if (method_exists($this, $method)) {
+            return $this->$method($call, $arguments);
+        }
+
+        return false;
+    }
+
+    /**
+     * Proxy to allow use of element names as attribute names
+     *
+     * For many elements variable-style access will be desirable. This function
+     * provides for that.
+     *
+     * @param   string  $value - the variable required
+     * @return  mixed
+     */
+    function __get($value)
+    {
+        return $this->__call($value, array());
+    }
+
+    /**
+     * Utility function to help us resolve xml:base values
+     *
+     * We have other methods which will traverse the DOM and work out the different
+     * xml:base declarations we need to be aware of. We then need to combine them.
+     * If a declaration starts with a protocol then we restart the string. If it 
+     * starts with a / then we add on to the domain name. Otherwise we simply tag 
+     * it on to the end.
+     *
+     * @param   string  $base - the base to add the link to
+     * @param   string  $link
+     */
+    function combineBases($base, $link)
+    {
+        if (preg_match('/^[A-Za-z]+:\/\//', $link)) {
+            return $link;
+        } else if (preg_match('/^\//', $link)) {
+            /* Extract domain and suffix link to that */
+            preg_match('/^([A-Za-z]+:\/\/.*)?\/*/', $base, $results);
+            $firstLayer = $results[0];
+            return $firstLayer . "/" . $link;
+        } else if (preg_match('/^\.\.\//', $base)) {
+            /* Step up link to find place to be */
+            preg_match('/^((\.\.\/)+)(.*)$/', $link, $bases);
+            $suffix = $bases[3];
+            $count = preg_match_all('/\.\.\//', $bases[1], $steps);
+            $url = explode("/", $base);
+            for ($i = 0; $i <= $count; $i++) {
+                array_pop($url);
+            }
+            return implode("/", $url) . "/" . $suffix;
+        } else if (preg_match('/^(?!\/$)/', $base)) {
+            $base = preg_replace('/(.*\/).*$/', '$1', $base)  ;
+            return $base . $link;
+        } else {
+            /* Just stick it on the end */
+            return $base . $link;
+        }
+    }
+
+    /**
+     * Determine whether we need to apply our xml:base rules
+     *
+     * Gets us the xml:base data and then processes that with regard
+     * to our current link.
+     *
+     * @param   string
+     * @param   DOMElement
+     * @return  string
+     */
+    function addBase($link, $element)
+    {
+        if (preg_match('/^[A-Za-z]+:\/\//', $link)) {
+            return $link;
+        }
+
+        return $this->combineBases($element->baseURI, $link);
+    }
+
+    /**
+     * Get an entry by its position in the feed, starting from zero
+     *
+     * As well as allowing the items to be iterated over we want to allow
+     * users to be able to access a specific entry. This is one of two ways of
+     * doing that, the other being by ID.
+     * 
+     * @param   int $offset
+     * @return  XML_Feed_Parser_RSS1Element
+     */
+    function getEntryByOffset($offset)
+    {
+        if (! isset($this->entries[$offset])) {
+            $entries = $this->model->getElementsByTagName($this->itemElement);
+            if ($entries->length > $offset) {
+                $xmlBase = $entries->item($offset)->baseURI;
+                $this->entries[$offset] = new $this->itemClass(
+                    $entries->item($offset), $this, $xmlBase);
+                if ($id = $this->entries[$offset]->id) {
+                    $this->idMappings[$id] = $this->entries[$offset];
+                }
+            } else {
+                throw new XML_Feed_Parser_Exception('No entries found');
+            }
+        }
+
+        return $this->entries[$offset];
+    }
+
+    /**
+     * Return a date in seconds since epoch.
+     *
+     * Get a date construct. We use PHP's strtotime to return it as a unix datetime, which
+     * is the number of seconds since 1970-01-01 00:00:00.
+     * 
+     * @link    http://php.net/strtotime
+     * @param    string    $method        The name of the date construct we want
+     * @param    array     $arguments    Included for compatibility with our __call usage
+     * @return    int|false datetime
+     */
+    protected function getDate($method, $arguments)
+    {
+        $time = $this->model->getElementsByTagName($method);
+        if ($time->length == 0) {
+            return false;
+        }
+        return strtotime($time->item(0)->nodeValue);
+    }
+
+    /**
+     * Get a text construct. 
+     *
+     * @param    string    $method    The name of the text construct we want
+     * @param    array     $arguments    Included for compatibility with our __call usage
+     * @return    string
+     */
+    protected function getText($method, $arguments = array())
+    {
+        $tags = $this->model->getElementsByTagName($method);
+        if ($tags->length > 0) {
+            $value = $tags->item(0)->nodeValue;
+            return $value;
+        }
+        return false;
+    }
+
+    /**
+     * Apply various rules to retrieve category data.
+     *
+     * There is no single way of declaring a category in RSS1/1.1 as there is in RSS2 
+     * and  Atom. Instead the usual approach is to use the dublin core namespace to 
+     * declare  categories. For example delicious use both: 
+     * <dc:subject>PEAR</dc:subject> and: <taxo:topics><rdf:Bag>
+     * <rdf:li resource="http://del.icio.us/tag/PEAR" /></rdf:Bag></taxo:topics>
+     * to declare a categorisation of 'PEAR'.
+     *
+     * We need to be sensitive to this where possible.
+     *
+     * @param    string    $call    for compatibility with our overloading
+     * @param   array $arguments - arg 0 is the offset, arg 1 is whether to return as array
+     * @return  string|array|false
+     */
+    protected function getCategory($call, $arguments)
+    {
+        $categories = $this->model->getElementsByTagName('subject');
+        $offset = empty($arguments[0]) ? 0 : $arguments[0];
+        $array = empty($arguments[1]) ? false : true;
+        if ($categories->length <= $offset) {
+            return false;
+        }
+        if ($array) {
+            $list = array();
+            foreach ($categories as $category) {
+                array_push($list, $category->nodeValue);
+            }
+            return $list;
+        }
+        return $categories->item($offset)->nodeValue;
+    }
+
+    /**
+     * Count occurrences of an element
+     *
+     * This function will tell us how many times the element $type
+     * appears at this level of the feed.
+     * 
+     * @param    string    $type    the element we want to get a count of
+     * @return    int
+     */
+    protected function count($type)
+    {
+        if ($tags = $this->model->getElementsByTagName($type)) {
+            return $tags->length;
+        }
+        return 0;
+    }
+
+    /**
+     * Part of our xml:base processing code
+     *
+     * We need a couple of methods to access XHTML content stored in feeds. 
+     * This is because we dereference all xml:base references before returning
+     * the element. This method handles the attributes.
+     *
+     * @param   DOMElement $node    The DOM node we are iterating over
+     * @return  string
+     */
+    function processXHTMLAttributes($node) {
+        $return = '';
+        foreach ($node->attributes as $attribute) {
+            if ($attribute->name == 'src' or $attribute->name == 'href') {
+                $attribute->value = $this->addBase($attribute->value, $attribute);
+            }
+            if ($attribute->name == 'base') {
+                continue;
+            }
+            $return .= $attribute->name . '="' . $attribute->value .'" ';
+        }
+        if (! empty($return)) {
+            return ' ' . trim($return);
+        }
+        return '';
+    }
+
+    /**
+     * Part of our xml:base processing code
+     *
+     * We need a couple of methods to access XHTML content stored in feeds. 
+     * This is because we dereference all xml:base references before returning
+     * the element. This method recurs through the tree descending from the node
+     * and builds our string
+     *
+     * @param   DOMElement $node    The DOM node we are processing
+     * @return   string
+     */
+    function traverseNode($node)
+    {
+        $content = '';
+
+        /* Add the opening of this node to the content */
+        if ($node instanceof DOMElement) {
+            $content .= '<' . $node->tagName . 
+                $this->processXHTMLAttributes($node) . '>';
+        }
+
+        /* Process children */
+        if ($node->hasChildNodes()) {
+            foreach ($node->childNodes as $child) {
+                $content .= $this->traverseNode($child);
+            }
+        }
+
+        if ($node instanceof DOMText) {
+            $content .= htmlentities($node->nodeValue);
+        }
+
+        /* Add the closing of this node to the content */
+        if ($node instanceof DOMElement) {
+            $content .= '</' . $node->tagName . '>';
+        }
+
+        return $content;
+    }
+
+    /**
+     * Get content from RSS feeds (atom has its own implementation)
+     *
+     * The official way to include full content in an RSS1 entry is to use
+     * the content module's element 'encoded', and RSS2 feeds often duplicate that.
+     * Often, however, the 'description' element is used instead. We will offer that 
+     * as a fallback. Atom uses its own approach and overrides this method.
+     *
+     * @return  string|false
+     */
+    protected function getContent()
+    {
+        $options = array('encoded', 'description');
+        foreach ($options as $element) {
+            $test = $this->model->getElementsByTagName($element);
+            if ($test->length == 0) {
+                continue;
+            }
+            if ($test->item(0)->hasChildNodes()) {
+                $value = '';
+                foreach ($test->item(0)->childNodes as $child) {
+                    if ($child instanceof DOMText) {
+                        $value .= $child->nodeValue;
+                    } else {
+                        $simple = simplexml_import_dom($child);
+                        $value .= $simple->asXML();
+                    }
+                }
+                return $value;
+            } else if ($test->length > 0) {
+                return $test->item(0)->nodeValue;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks if this element has a particular child element.
+     *
+     * @param   String
+     * @param   Integer
+     * @return  bool
+     **/
+    function hasKey($name, $offset = 0)
+    {
+        $search = $this->model->getElementsByTagName($name);
+        return $search->length > $offset;
+    }
+
+    /**
+     * Return an XML serialization of the feed, should it be required. Most 
+     * users however, will already have a serialization that they used when 
+     * instantiating the object.
+     *
+     * @return    string    XML serialization of element
+     */    
+    function __toString()
+    {
+        $simple = simplexml_import_dom($this->model);
+        return $simple->asXML();
+    }
+    
+    /**
+     * Get directory holding RNG schemas. Method is based on that 
+     * found in Contact_AddressBook.
+     *
+     * @return string PEAR data directory.
+     * @access public
+     * @static
+     */
+    static function getSchemaDir()
+    {
+        require_once 'PEAR/Config.php';
+        $config = new PEAR_Config;
+        return $config->get('data_dir') . '/XML_Feed_Parser/schemas';
+    }
+}
+
+?>
\ No newline at end of file
diff --git a/lib/general.php b/lib/general.php
new file mode 100644 (file)
index 0000000..fab6aa5
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+ini_set('include_path', get_config('lib'));
+
+function get_config($key) {
+    global $CFG;
+
+    if (array_key_exists($key, $CFG)) {
+       return $CFG[$key];
+    }
+    return false;
+
+}
+
+function get_components() {
+
+    $comps = array();
+    $dirhandle = opendir(get_config('comp'));
+    while (false !== ($file = readdir($dirhandle))) {
+        if (strpos($file, '.') === 0) {
+            continue;
+        }
+        $comps[] = substr($file, 0, strpos($file, '.'));
+    }
+    return $comps;
+}
+
+function parse_rss($url) {
+    require_once('Cache/Lite.php');
+
+    // Set a few options
+    $options = array(
+        'cacheDir' => get_config('rss_cache'),
+        'lifeTime' => get_config('cache_lifetime'),
+        'automaticSerialization' => true,
+    );
+
+    // Create a cache object
+    $cache = new Cache_Lite($options);
+
+    // Test if thereis a valide cache for this id
+    if (!$data = $cache->get($url)) {
+
+        require_once('XML/Feed/Parser.php');
+        require_once('Snoopy.class.php');
+
+        $snoopy = new Snoopy();
+        $snoopy->curl_path = get_config('curl');
+
+        // This is to disable warnings from snoopy while it performs its work.
+        // By all means, comment out while debugging any problems
+        $oldlevel = error_reporting(0);
+        $result = $snoopy->fetch($url);
+        error_reporting($oldlevel);
+
+        if (!$result) {
+            $e = new XML_Feed_Parser_Exception($snoopy->error);
+            $cache->save($e, $url);
+            return false;
+        }
+
+        try {
+            $feed = new XML_Feed_Parser($snoopy->results, false, true, false);
+        }
+        catch (XML_Feed_Parser_Exception $e) {
+            $cache_Lite->save($e, $url);
+            return false;
+            // Don't catch other exceptions, they're an indication something
+            // really bad happened
+        }
+
+        $data = new StdClass;
+        $data->title = $feed->title;
+        $data->url = $url;
+        $data->link = $feed->link;
+        $data->description = $feed->description;
+        $data->content = array();
+        foreach ($feed as $count => $item) {
+            if ($count == 11) { // it starts at one!
+                break;
+            }
+            $data->content[] = (object)array('title' => $item->title, 'link' => $item->link, 'content' => $item->content);
+        }
+        $cache->save($data, $url);
+    }
+
+    if ($data instanceof XML_Feed_Parser_Exception) {
+        return false;
+    }
+    return $data;
+}
+?>
diff --git a/lib/phpFlickr.php b/lib/phpFlickr.php
new file mode 100644 (file)
index 0000000..f1c194c
--- /dev/null
@@ -0,0 +1,1414 @@
+<?php
+/* phpFlickr Class 2.2.0
+ * Written by Dan Coulter (dan@dancoulter.com)
+ * Sourceforge Project Page: http://www.sourceforge.net/projects/phpflickr/
+ * Released under GNU Lesser General Public License (http://www.gnu.org/copyleft/lgpl.html)
+ * For more information about the class and upcoming tools and toys using it,
+ * visit http://www.phpflickr.com/ or http://phpflickr.sourceforge.net
+ *
+ *      For installation instructions, open the README.txt file packaged with this
+ *      class. If you don't have a copy, you can see it at:
+ *      http://www.phpflickr.com/README.txt
+ *
+ *      Please submit all problems or questions to the Help Forum on my project page:
+ *              http://sourceforge.net/forum/forum.php?forum_id=469652
+ *
+ */
+if (session_id() == "") {
+       @session_start();
+}
+
+// Decides which include path delimiter to use.  Windows should be using a semi-colon
+// and everything else should be using a colon.  If this isn't working on your system,
+// comment out this if statement and manually set the correct value into $path_delimiter.
+if (strpos(__FILE__, ':') !== false) {
+       $path_delimiter = ';';
+} else {
+       $path_delimiter = ':';
+}
+
+// This will add the packaged PEAR files into the include path for PHP, allowing you
+// to use them transparently.  This will prefer officially installed PEAR files if you
+// have them.  If you want to prefer the packaged files (there shouldn't be any reason
+// to), swap the two elements around the $path_delimiter variable.  If you don't have
+// the PEAR packages installed, you can leave this like it is and move on.
+
+ini_set('include_path', ini_get('include_path') . $path_delimiter . dirname(__FILE__) . '/PEAR');
+
+// If you have problems including the default PEAR install (like if your open_basedir
+// setting doesn't allow you to include files outside of your web root), comment out
+// the line above and uncomment the next line:
+
+// ini_set('include_path', dirname(__FILE__) . '/PEAR' . $path_delimiter . ini_get('include_path'));
+
+class phpFlickr {
+       var $api_key;
+       var $secret;
+       var $REST = 'http://api.flickr.com/services/rest/';
+       var $Upload = 'http://api.flickr.com/services/upload/';
+       var $Replace = 'http://api.flickr.com/services/replace/';
+       var $req;
+       var $response;
+       var $parsed_response;
+       var $cache = false;
+       var $cache_db = null;
+       var $cache_table = null;
+       var $cache_dir = null;
+       var $cache_expire = null;
+       var $die_on_error;
+       var $error_code;
+       Var $error_msg;
+       var $token;
+       var $php_version;
+
+       /*
+        * When your database cache table hits this many rows, a cleanup
+        * will occur to get rid of all of the old rows and cleanup the
+        * garbage in the table.  For most personal apps, 1000 rows should
+        * be more than enough.  If your site gets hit by a lot of traffic
+        * or you have a lot of disk space to spare, bump this number up.
+        * You should try to set it high enough that the cleanup only
+        * happens every once in a while, so this will depend on the growth
+        * of your table.
+        */
+       var $max_cache_rows = 1000;
+
+       function phpFlickr ($api_key, $secret = NULL, $die_on_error = false)
+       {
+               //The API Key must be set before any calls can be made.  You can
+               //get your own at http://www.flickr.com/services/api/misc.api_keys.html
+               $this->api_key = $api_key;
+               $this->secret = $secret;
+               $this->die_on_error = $die_on_error;
+               $this->service = "flickr";
+
+               //Find the PHP version and store it for future reference
+               $this->php_version = explode("-", phpversion());
+               $this->php_version = explode(".", $this->php_version[0]);
+
+               //All calls to the API are done via the POST method using the PEAR::HTTP_Request package.
+               require_once 'HTTP/Request.php';
+               $this->req =& new HTTP_Request();
+               $this->req->setMethod(HTTP_REQUEST_METHOD_POST);
+       }
+
+       function enableCache($type, $connection, $cache_expire = 600, $table = 'flickr_cache')
+       {
+               // Turns on caching.  $type must be either "db" (for database caching) or "fs" (for filesystem).
+               // When using db, $connection must be a PEAR::DB connection string. Example:
+               //        "mysql://user:password@server/database"
+               // If the $table, doesn't exist, it will attempt to create it.
+               // When using file system, caching, the $connection is the folder that the web server has write
+               // access to. Use absolute paths for best results.  Relative paths may have unexpected behavior
+               // when you include this.  They'll usually work, you'll just want to test them.
+               if ($type == 'db') {
+                       require_once 'DB.php';
+                       $db =& DB::connect($connection);
+                       if (PEAR::isError($db)) {
+                               die($db->getMessage());
+                       }
+
+                       /*
+                        * If high performance is crucial, you can easily comment
+                        * out this query once you've created your database table.
+                        */
+
+                       $db->query("
+                               CREATE TABLE IF NOT EXISTS `$table` (
+                                       `request` CHAR( 35 ) NOT NULL ,
+                                       `response` MEDIUMTEXT NOT NULL ,
+                                       `expiration` DATETIME NOT NULL ,
+                                       INDEX ( `request` )
+                               ) TYPE = MYISAM");
+
+                       if ($db->getOne("SELECT COUNT(*) FROM $table") > $this->max_cache_rows) {
+                               $db->query("DELETE FROM $table WHERE expiration < DATE_SUB(NOW(), INTERVAL $cache_expire second)");
+                               $db->query('OPTIMIZE TABLE ' . $this->cache_table);
+                       }
+
+                       $this->cache = 'db';
+                       $this->cache_db = $db;
+                       $this->cache_table = $table;
+               } elseif ($type == 'fs') {
+                       $this->cache = 'fs';
+                       $connection = realpath($connection);
+                       $this->cache_dir = $connection;
+                       if ($dir = opendir($this->cache_dir)) {
+                               while ($file = readdir($dir)) {
+                                       if (substr($file, -6) == '.cache' && ((filemtime($this->cache_dir . '/' . $file) + $cache_expire) < time()) ) {
+                                               unlink($this->cache_dir . '/' . $file);
+                                       }
+                               }
+                       }
+               }
+               $this->cache_expire = $cache_expire;
+       }
+
+       function getCached ($request)
+       {
+               //Checks the database or filesystem for a cached result to the request.
+               //If there is no cache result, it returns a value of false. If it finds one,
+               //it returns the unparsed XML.
+               $reqhash = md5(serialize($request));
+               if ($this->cache == 'db') {
+                       $result = $this->cache_db->getOne("SELECT response FROM " . $this->cache_table . " WHERE request = ? AND DATE_SUB(NOW(), INTERVAL " . (int) $this->cache_expire . " SECOND) < expiration", $reqhash);
+                       if (!empty($result)) {
+                               return $result;
+                       }
+               } elseif ($this->cache == 'fs') {
+                       $file = $this->cache_dir . '/' . $reqhash . '.cache';
+                       if (file_exists($file)) {
+                               if ($this->php_version[0] > 4 || ($this->php_version[0] == 4 && $this->php_version[1] >= 3)) {
+                                       return file_get_contents($file);
+                               } else {
+                                       return implode('', file($file));
+                               }
+                       }
+               }
+               return false;
+       }
+
+       function cache ($request, $response)
+       {
+               //Caches the unparsed XML of a request.
+               $reqhash = md5(serialize($request));
+               if ($this->cache == 'db') {
+                       //$this->cache_db->query("DELETE FROM $this->cache_table WHERE request = '$reqhash'");
+                       if ($this->cache_db->getOne("SELECT COUNT(*) FROM {$this->cache_table} WHERE request = '$reqhash'")) {
+                               $sql = "UPDATE " . $this->cache_table . " SET response = ?, expiration = ? WHERE request = ?";
+                               $this->cache_db->query($sql, array($response, strftime("%Y-%m-%d %H:%M:%S"), $reqhash));
+                       } else {
+                               $sql = "INSERT INTO " . $this->cache_table . " (request, response, expiration) VALUES ('$reqhash', '" . str_replace("'", "''", $response) . "', '" . strftime("%Y-%m-%d %H:%M:%S") . "')";
+                               $this->cache_db->query($sql);
+                       }
+               } elseif ($this->cache == "fs") {
+                       $file = $this->cache_dir . "/" . $reqhash . ".cache";
+                       $fstream = fopen($file, "w");
+                       $result = fwrite($fstream,$response);
+                       fclose($fstream);
+                       return $result;
+               }
+               return false;
+       }
+
+       function request ($command, $args = array(), $nocache = false)
+       {
+               //Sends a request to Flickr's REST endpoint via POST.
+               $this->req->setURL($this->REST);
+               $this->req->clearPostData();
+               if (substr($command,0,7) != "flickr.") {
+                       $command = "flickr." . $command;
+               }
+
+               //Process arguments, including method and login data.
+               $args = array_merge(array("method" => $command, "format" => "php_serial", "api_key" => $this->api_key), $args);
+               if (!empty($this->token)) {
+                       $args = array_merge($args, array("auth_token" => $this->token));
+               } elseif (!empty($_SESSION['phpFlickr_auth_token'])) {
+                       $args = array_merge($args, array("auth_token" => $_SESSION['phpFlickr_auth_token']));
+               }
+               ksort($args);
+               $auth_sig = "";
+               if (!($this->response = $this->getCached($args)) || $nocache) {
+                       foreach ($args as $key => $data) {
+                               $auth_sig .= $key . $data;
+                               $this->req->addPostData($key, $data);
+                       }
+                       if (!empty($this->secret)) {
+                               $api_sig = md5($this->secret . $auth_sig);
+                               $this->req->addPostData("api_sig", $api_sig);
+                       }
+
+                       $this->req->addHeader("Connection", "Keep-Alive");
+                       
+                       //Send Requests
+                       if ($this->req->sendRequest()) {
+                               $this->response = $this->req->getResponseBody();
+                               $this->cache($args, $this->response);
+                       } else {
+                               die("There has been a problem sending your command to the server.");
+                       }
+               }
+               /*
+                * Uncomment this line (and comment out the next one) if you're doing large queries
+                * and you're concerned about time.  This will, however, change the structure of
+                * the result, so be sure that you look at the results.
+                */
+               //$this->parsed_response = unserialize($this->response);
+               $this->parsed_response = $this->clean_text_nodes(unserialize($this->response));
+               if ($this->parsed_response['stat'] == 'fail') {
+                       if ($this->die_on_error) die("The Flickr API returned the following error: #{$this->parsed_response['code']} - {$this->parsed_response['message']}");
+                       else {
+                               $this->error_code = $this->parsed_response['code'];
+                               $this->error_msg = $this->parsed_response['message'];
+                               $this->parsed_response = false;
+                       }
+               } else {
+                       $this->error_code = false;
+                       $this->error_msg = false;
+               }
+               return $this->response;
+       }
+
+       function clean_text_nodes($arr) {
+               if (!is_array($arr)) {
+                       return $arr;
+               } elseif (count($arr) == 0) {
+                       return $arr;
+               } elseif (count($arr) == 1 && array_key_exists('_content', $arr)) {
+                       return $arr['_content'];
+               } else {
+                       foreach ($arr as $key => $element) {
+                               $arr[$key] = $this->clean_text_nodes($element);
+                       }
+                       return($arr);
+               }
+       }
+
+       function setToken($token)
+       {
+               // Sets an authentication token to use instead of the session variable
+               $this->token = $token;
+       }
+
+       function setProxy($server, $port)
+       {
+               // Sets the proxy for all phpFlickr calls.
+               $this->req->setProxy($server, $port);
+       }
+
+       function getErrorCode()
+       {
+               // Returns the error code of the last call.  If the last call did not
+               // return an error. This will return a false boolean.
+               return $this->error_code;
+       }
+
+       function getErrorMsg()
+       {
+               // Returns the error message of the last call.  If the last call did not
+               // return an error. This will return a false boolean.
+               return $this->error_msg;
+       }
+
+       /* These functions are front ends for the flickr calls */
+
+       function buildPhotoURL ($photo, $size = "Medium")
+       {
+               //receives an array (can use the individual photo data returned
+               //from an API call) and returns a URL (doesn't mean that the
+               //file size exists)
+               $sizes = array(
+                       "square" => "_s",
+                       "thumbnail" => "_t",
+                       "small" => "_m",
+                       "medium" => "",
+                       "large" => "_b",
+                       "original" => "_o"
+               );
+               
+               $size = strtolower($size);
+               if (!array_key_exists($size, $sizes)) {
+                       $size = "medium";
+               }
+               
+               if ($size == "original") {
+                       $url = "http://farm" . $photo['farm'] . ".static.flickr.com/" . $photo['server'] . "/" . $photo['id'] . "_" . $photo['originalsecret'] . "_o" . "." . $photo['originalformat'];
+               } else {
+                       $url = "http://farm" . $photo['farm'] . ".static.flickr.com/" . $photo['server'] . "/" . $photo['id'] . "_" . $photo['secret'] . $sizes[$size] . ".jpg";
+               }
+               return $url;
+       }
+
+       function getFriendlyGeodata($lat, $lon) {
+               /* I've added this method to get the friendly geodata (i.e. 'in New York, NY') that the
+                * website provides, but isn't available in the API. I'm providing this service as long
+                * as it doesn't flood my server with requests and crash it all the time.
+                */
+               return unserialize(file_get_contents('http://phpflickr.com/geodata/?format=php&lat=' . $lat . '&lon=' . $lon));
+       }
+
+       function sync_upload ($photo, $title = null, $description = null, $tags = null, $is_public = null, $is_friend = null, $is_family = null) {
+               $upload_req =& new HTTP_Request();
+               $upload_req->setMethod(HTTP_REQUEST_METHOD_POST);
+
+
+               $upload_req->setURL($this->Upload);
+               $upload_req->clearPostData();
+
+               //Process arguments, including method and login data.
+               $args = array("api_key" => $this->api_key, "title" => $title, "description" => $description, "tags" => $tags, "is_public" => $is_public, "is_friend" => $is_friend, "is_family" => $is_family);
+               if (!empty($this->email)) {
+                       $args = array_merge($args, array("email" => $this->email));
+               }
+               if (!empty($this->password)) {
+                       $args = array_merge($args, array("password" => $this->password));
+               }
+               if (!empty($this->token)) {
+                       $args = array_merge($args, array("auth_token" => $this->token));
+               } elseif (!empty($_SESSION['phpFlickr_auth_token'])) {
+                       $args = array_merge($args, array("auth_token" => $_SESSION['phpFlickr_auth_token']));
+               }
+
+               ksort($args);
+               $auth_sig = "";
+               foreach ($args as $key => $data) {
+                       if ($data !== null) {
+                               $auth_sig .= $key . $data;
+                               $upload_req->addPostData($key, $data);
+                       }
+               }
+               if (!empty($this->secret)) {
+                       $api_sig = md5($this->secret . $auth_sig);
+                       $upload_req->addPostData("api_sig", $api_sig);
+               }
+
+               $photo = realpath($photo);
+
+               $result = $upload_req->addFile("photo", $photo);
+
+               if (PEAR::isError($result)) {
+                       die($result->getMessage());
+               }
+
+               //Send Requests
+               if ($upload_req->sendRequest()) {
+                       $this->response = $upload_req->getResponseBody();
+               } else {
+                       die("There has been a problem sending your command to the server.");
+               }
+
+               $rsp = explode("\n", $this->response);
+               foreach ($rsp as $line) {
+                       if (ereg('<err code="([0-9]+)" msg="(.*)"', $line, $match)) {
+                               if ($this->die_on_error)
+                                       die("The Flickr API returned the following error: #{$match[1]} - {$match[2]}");
+                               else {
+                                       $this->error_code = $match[1];
+                                       $this->error_msg = $match[2];
+                                       $this->parsed_response = false;
+                                       return false;
+                               }
+                       } elseif (ereg("<photoid>(.*)</photoid>", $line, $match)) {
+                               $this->error_code = false;
+                               $this->error_msg = false;
+                               return $match[1];
+                       }
+               }
+       }
+
+       function async_upload ($photo, $title = null, $description = null, $tags = null, $is_public = null, $is_friend = null, $is_family = null) {
+               $upload_req =& new HTTP_Request();
+               $upload_req->setMethod(HTTP_REQUEST_METHOD_POST);
+
+               $upload_req->setURL($this->Upload);
+               $upload_req->clearPostData();
+
+               //Process arguments, including method and login data.
+               $args = array("async" => 1, "api_key" => $this->api_key, "title" => $title, "description" => $description, "tags" => $tags, "is_public" => $is_public, "is_friend" => $is_friend, "is_family" => $is_family);
+               if (!empty($this->email)) {
+                       $args = array_merge($args, array("email" => $this->email));
+               }
+               if (!empty($this->password)) {
+                       $args = array_merge($args, array("password" => $this->password));
+               }
+               if (!empty($this->token)) {
+                       $args = array_merge($args, array("auth_token" => $this->token));
+               } elseif (!empty($_SESSION['phpFlickr_auth_token'])) {
+                       $args = array_merge($args, array("auth_token" => $_SESSION['phpFlickr_auth_token']));
+               }
+
+               ksort($args);
+               $auth_sig = "";
+               foreach ($args as $key => $data) {
+                       if ($data !== null) {
+                               $auth_sig .= $key . $data;
+                               $upload_req->addPostData($key, $data);
+                       }
+               }
+               if (!empty($this->secret)) {
+                       $api_sig = md5($this->secret . $auth_sig);
+                       $upload_req->addPostData("api_sig", $api_sig);
+               }
+
+               $photo = realpath($photo);
+
+               $result = $upload_req->addFile("photo", $photo);
+
+               if (PEAR::isError($result)) {
+                       die($result->getMessage());
+               }
+
+               //Send Requests
+               if ($upload_req->sendRequest()) {
+                       $this->response = $upload_req->getResponseBody();
+               } else {
+                       die("There has been a problem sending your command to the server.");
+               }
+
+               $rsp = explode("\n", $this->response);
+               foreach ($rsp as $line) {
+                       if (ereg('<err code="([0-9]+)" msg="(.*)"', $line, $match)) {
+                               if ($this->die_on_error)
+                                       die("The Flickr API returned the following error: #{$match[1]} - {$match[2]}");
+                               else {
+                                       $this->error_code = $match[1];
+                                       $this->error_msg = $match[2];
+                                       $this->parsed_response = false;
+                                       return false;
+                               }
+                       } elseif (ereg("<ticketid>(.*)</", $line, $match)) {
+                               $this->error_code = false;
+                               $this->error_msg = false;
+                               return $match[1];
+                       }
+               }
+       }
+
+       // Interface for new replace API method.
+       function replace ($photo, $photo_id, $async = null) {
+               $upload_req =& new HTTP_Request();
+               $upload_req->setMethod(HTTP_REQUEST_METHOD_POST);
+
+               $upload_req->setURL($this->Replace);
+               $upload_req->clearPostData();
+
+               //Process arguments, including method and login data.
+               $args = array("api_key" => $this->api_key, "photo_id" => $photo_id, "async" => $async);
+               if (!empty($this->email)) {
+                       $args = array_merge($args, array("email" => $this->email));
+               }
+               if (!empty($this->password)) {
+                       $args = array_merge($args, array("password" => $this->password));
+               }
+               if (!empty($this->token)) {
+                       $args = array_merge($args, array("auth_token" => $this->token));
+               } elseif (!empty($_SESSION['phpFlickr_auth_token'])) {
+                       $args = array_merge($args, array("auth_token" => $_SESSION['phpFlickr_auth_token']));
+               }
+
+               ksort($args);
+               $auth_sig = "";
+               foreach ($args as $key => $data) {
+                       if ($data !== null) {
+                               $auth_sig .= $key . $data;
+                               $upload_req->addPostData($key, $data);
+                       }
+               }
+               if (!empty($this->secret)) {
+                       $api_sig = md5($this->secret . $auth_sig);
+                       $upload_req->addPostData("api_sig", $api_sig);
+               }
+
+               $photo = realpath($photo);
+
+               $result = $upload_req->addFile("photo", $photo);
+
+               if (PEAR::isError($result)) {
+                       die($result->getMessage());
+               }
+
+               //Send Requests
+               if ($upload_req->sendRequest()) {
+                       $this->response = $upload_req->getResponseBody();
+               } else {
+                       die("There has been a problem sending your command to the server.");
+               }
+               if ($async == 1)
+                       $find = 'ticketid';
+                else
+                       $find = 'photoid';
+
+               $rsp = explode("\n", $this->response);
+               foreach ($rsp as $line) {
+                       if (ereg('<err code="([0-9]+)" msg="(.*)"', $line, $match)) {
+                               if ($this->die_on_error)
+                                       die("The Flickr API returned the following error: #{$match[1]} - {$match[2]}");
+                               else {
+                                       $this->error_code = $match[1];
+                                       $this->error_msg = $match[2];
+                                       $this->parsed_response = false;
+                                       return false;
+                               }
+                       } elseif (ereg("<" . $find . ">(.*)</", $line, $match)) {
+                               $this->error_code = false;
+                               $this->error_msg = false;
+                               return $match[1];
+                       }
+               }
+       }
+
+       function auth ($perms = "read", $remember_uri = true)
+       {
+               // Redirects to Flickr's authentication piece if there is no valid token.
+               // If remember_uri is set to false, the callback script (included) will
+               // redirect to its default page.
+
+               if (empty($_SESSION['phpFlickr_auth_token']) && empty($this->token)) {
+                       if ($remember_uri) {
+                               $redirect = $_SERVER['REQUEST_URI'];
+                       }
+                       $api_sig = md5($this->secret . "api_key" . $this->api_key . "extra" . $redirect . "perms" . $perms);
+                       if ($this->service == "23") {
+                               header("Location: http://www.23hq.com/services/auth/?api_key=" . $this->api_key . "&extra=" . $redirect . "&perms=" . $perms . "&api_sig=". $api_sig);
+                       } else {
+                               header("Location: http://www.flickr.com/services/auth/?api_key=" . $this->api_key . "&extra=" . $redirect . "&perms=" . $perms . "&api_sig=". $api_sig);
+                       }
+                       exit;
+               } else {
+                       $tmp = $this->die_on_error;
+                       $this->die_on_error = false;
+                       $rsp = $this->auth_checkToken();
+                       if ($this->error_code !== false) {
+                               unset($_SESSION['phpFlickr_auth_token']);
+                               $this->auth($perms, $remember_uri);
+                       }
+                       $this->die_on_error = $tmp;
+                       return $rsp['perms'];
+               }
+       }
+
+       /*******************************
+
+       To use the phpFlickr::call method, pass a string containing the API method you want
+       to use and an associative array of arguments.  For example:
+               $result = $f->call("flickr.photos.comments.getList", array("photo_id"=>'34952612'));
+       This method will allow you to make calls to arbitrary methods that haven't been
+       implemented in phpFlickr yet.
+
+       *******************************/
+
+       function call($method, $arguments)
+       {
+               $this->request($method, $arguments);
+               return $this->parsed_response ? $this->parsed_response : false;
+       }
+
+       /*
+               These functions are the direct implementations of flickr calls.
+               For method documentation, including arguments, visit the address
+               included in a comment in the function.
+       */
+
+       /* Activity methods */
+       function activity_userComments ($per_page = NULL, $page = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.activity.userComments.html */
+               $this->request('flickr.activity.userComments', array("per_page" => $per_page, "page" => $page));
+               return $this->parsed_response ? $this->parsed_response['items']['item'] : false;
+       }
+
+       function activity_userPhotos ($timeframe = NULL, $per_page = NULL, $page = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.activity.userPhotos.html */
+               $this->request('flickr.activity.userPhotos', array("timeframe" => $timeframe, "per_page" => $per_page, "page" => $page));
+               return $this->parsed_response ? $this->parsed_response['items']['item'] : false;
+       }
+
+       /* Authentication methods */
+       function auth_checkToken ()
+       {
+               /* http://www.flickr.com/services/api/flickr.auth.checkToken.html */
+               $this->request('flickr.auth.checkToken');
+               return $this->parsed_response ? $this->parsed_response['auth'] : false;
+       }
+
+       function auth_getFrob ()
+       {
+               /* http://www.flickr.com/services/api/flickr.auth.getFrob.html */
+               $this->request('flickr.auth.getFrob');
+               return $this->parsed_response ? $this->parsed_response['frob'] : false;
+       }
+
+       function auth_getFullToken ($mini_token)
+       {
+               /* http://www.flickr.com/services/api/flickr.auth.getFullToken.html */
+               $this->request('flickr.auth.getFullToken', array('mini_token'=>$mini_token));
+               return $this->parsed_response ? $this->parsed_response['auth'] : false;
+       }
+
+       function auth_getToken ($frob)
+       {
+               /* http://www.flickr.com/services/api/flickr.auth.getToken.html */
+               $this->request('flickr.auth.getToken', array('frob'=>$frob));
+               session_register('phpFlickr_auth_token');
+               $_SESSION['phpFlickr_auth_token'] = $this->parsed_response['auth']['token'];
+               return $this->parsed_response ? $this->parsed_response['auth'] : false;
+       }
+
+       /* Blogs methods */
+       function blogs_getList ()
+       {
+               /* http://www.flickr.com/services/api/flickr.blogs.getList.html */
+               $this->request('flickr.blogs.getList');
+               return $this->parsed_response ? $this->parsed_response['blogs']['blog'] : false;
+       }
+
+       function blogs_postPhoto($blog_id, $photo_id, $title, $description, $blog_password = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.blogs.postPhoto.html */
+               $this->request('flickr.blogs.postPhoto', array('blog_id'=>$blog_id, 'photo_id'=>$photo_id, 'title'=>$title, 'description'=>$description, 'blog_password'=>$blog_password), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       /* Contacts Methods */
+       function contacts_getList ($filter = NULL, $page = NULL, $per_page = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.contacts.getList.html */
+               $this->request('flickr.contacts.getList', array('filter'=>$filter, 'page'=>$page, 'per_page'=>$per_page));
+               return $this->parsed_response ? $this->parsed_response['contacts'] : false;
+       }
+
+       function contacts_getPublicList($user_id, $page = NULL, $per_page = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.contacts.getPublicList.html */
+               $this->request('flickr.contacts.getPublicList', array('user_id'=>$user_id, 'page'=>$page, 'per_page'=>$per_page));
+               return $this->parsed_response ? $this->parsed_response['contacts'] : false;
+       }
+
+       /* Favorites Methods */
+       function favorites_add ($photo_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.favorites.add.html */
+               $this->request('flickr.favorites.add', array('photo_id'=>$photo_id), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       function favorites_getList($user_id = NULL, $extras = NULL, $per_page = NULL, $page = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.favorites.getList.html */
+               if (is_array($extras)) { $extras = implode(",", $extras); }
+               $this->request("flickr.favorites.getList", array("user_id"=>$user_id, "extras"=>$extras, "per_page"=>$per_page, "page"=>$page));
+               return $this->parsed_response ? $this->parsed_response['photos'] : false;
+       }
+
+       function favorites_getPublicList($user_id = NULL, $extras = NULL, $per_page = NULL, $page = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.favorites.getPublicList.html */
+               if (is_array($extras)) {
+                       $extras = implode(",", $extras);
+               }
+               $this->request("flickr.favorites.getPublicList", array("user_id"=>$user_id, "extras"=>$extras, "per_page"=>$per_page, "page"=>$page));
+               return $this->parsed_response ? $this->parsed_response['photos'] : false;
+       }
+
+       function favorites_remove($photo_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.favorites.remove.html */
+               $this->request("flickr.favorites.remove", array("photo_id"=>$photo_id), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       /* Groups Methods */
+       function groups_browse ($cat_id = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.groups.browse.html */
+               $this->request("flickr.groups.browse", array("cat_id"=>$cat_id));
+               return $this->parsed_response ? $this->parsed_response['category'] : false;
+       }
+
+       function groups_getInfo ($group_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.groups.getInfo.html */
+               $this->request("flickr.groups.getInfo", array("group_id"=>$group_id));
+               return $this->parsed_response ? $this->parsed_response['group'] : false;
+       }
+
+       function groups_search ($text, $per_page=NULL, $page=NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.groups.search.html */
+               $this->request("flickr.groups.search", array("text"=>$text,"per_page"=>$per_page,"page"=>$page));
+               return $this->parsed_response ? $this->parsed_response['groups'] : false;
+       }
+
+       /* Groups Pools Methods */
+       function groups_pools_add ($photo_id, $group_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.groups.pools.add.html */
+               $this->request("flickr.groups.pools.add", array("photo_id"=>$photo_id, "group_id"=>$group_id), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       function groups_pools_getContext ($photo_id, $group_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.groups.pools.getContext.html */
+               $this->request("flickr.groups.pools.getContext", array("photo_id"=>$photo_id, "group_id"=>$group_id));
+               return $this->parsed_response ? $this->parsed_response : false;
+       }
+
+       function groups_pools_getGroups ($page = NULL, $per_page = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.groups.pools.getGroups.html */
+               $this->request("flickr.groups.pools.getGroups", array('page'=>$page, 'per_page'=>$per_page));
+               return $this->parsed_response ? $this->parsed_response['groups'] : false;
+       }
+
+       function groups_pools_getPhotos ($group_id, $tags = NULL, $user_id = NULL, $extras = NULL, $per_page = NULL, $page = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.groups.pools.getPhotos.html */
+               if (is_array($extras)) {
+                       $extras = implode(",", $extras);
+               }
+               $this->request("flickr.groups.pools.getPhotos", array("group_id"=>$group_id, "tags"=>$tags, "user_id"=>$user_id, "extras"=>$extras, "per_page"=>$per_page, "page"=>$page));
+               return $this->parsed_response ? $this->parsed_response['photos'] : false;
+       }
+
+       function groups_pools_remove ($photo_id, $group_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.groups.pools.remove.html */
+               $this->request("flickr.groups.pools.remove", array("photo_id"=>$photo_id, "group_id"=>$group_id), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       /* Interestingness methods */
+       function interestingness_getList($date = NULL, $extras = NULL, $per_page = NULL, $page = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.interestingness.getList.html */
+               if (is_array($extras)) {
+                       $extras = implode(",", $extras);
+               }
+
+               $this->request("flickr.interestingness.getList", array("date"=>$date, "extras"=>$extras, "per_page"=>$per_page, "page"=>$page));
+               return $this->parsed_response ? $this->parsed_response['photos'] : false;
+       }
+
+       /* People methods */
+       function people_findByEmail ($find_email)
+       {
+               /* http://www.flickr.com/services/api/flickr.people.findByEmail.html */
+               $this->request("flickr.people.findByEmail", array("find_email"=>$find_email));
+               return $this->parsed_response ? $this->parsed_response['user'] : false;
+       }
+
+       function people_findByUsername ($username)
+       {
+               /* http://www.flickr.com/services/api/flickr.people.findByUsername.html */
+               $this->request("flickr.people.findByUsername", array("username"=>$username));
+               return $this->parsed_response ? $this->parsed_response['user'] : false;
+       }
+
+       function people_getInfo($user_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.people.getInfo.html */
+               $this->request("flickr.people.getInfo", array("user_id"=>$user_id));
+               return $this->parsed_response ? $this->parsed_response['person'] : false;
+       }
+
+       function people_getPublicGroups($user_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.people.getPublicGroups.html */
+               $this->request("flickr.people.getPublicGroups", array("user_id"=>$user_id));
+               return $this->parsed_response ? $this->parsed_response['groups']['group'] : false;
+       }
+
+       function people_getPublicPhotos($user_id, $extras = NULL, $per_page = NULL, $page = NULL) {
+               /* http://www.flickr.com/services/api/flickr.people.getPublicPhotos.html */
+               if (is_array($extras)) {
+                       $extras = implode(",", $extras);
+               }
+
+               $this->request("flickr.people.getPublicPhotos", array("user_id"=>$user_id, "extras"=>$extras, "per_page"=>$per_page, "page"=>$page));
+               return $this->parsed_response ? $this->parsed_response['photos'] : false;
+       }
+
+       function people_getUploadStatus()
+       {
+               /* http://www.flickr.com/services/api/flickr.people.getUploadStatus.html */
+               /* Requires Authentication */
+               $this->request("flickr.people.getUploadStatus");
+               return $this->parsed_response ? $this->parsed_response['user'] : false;
+       }
+
+
+       /* Photos Methods */
+       function photos_addTags ($photo_id, $tags)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.addTags.html */
+               $this->request("flickr.photos.addTags", array("photo_id"=>$photo_id, "tags"=>$tags), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       function photos_delete($photo_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.delete.html */
+               $this->request("flickr.photos.delete", array("photo_id"=>$photo_id), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       function photos_getAllContexts ($photo_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.getAllContexts.html */
+               $this->request("flickr.photos.getAllContexts", array("photo_id"=>$photo_id));
+               return $this->parsed_response ? $this->parsed_response : false;
+       }
+
+       function photos_getContactsPhotos ($count = NULL, $just_friends = NULL, $single_photo = NULL, $include_self = NULL, $extras = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.getContactsPhotos.html */
+               $this->request("flickr.photos.getContactsPhotos", array("count"=>$count, "just_friends"=>$just_friends, "single_photo"=>$single_photo, "include_self"=>$include_self, "extras"=>$extras));
+               return $this->parsed_response ? $this->parsed_response['photos']['photo'] : false;
+       }
+
+       function photos_getContactsPublicPhotos ($user_id, $count = NULL, $just_friends = NULL, $single_photo = NULL, $include_self = NULL, $extras = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.getContactsPublicPhotos.html */
+               $this->request("flickr.photos.getContactsPublicPhotos", array("user_id"=>$user_id, "count"=>$count, "just_friends"=>$just_friends, "single_photo"=>$single_photo, "include_self"=>$include_self, "extras"=>$extras));
+               return $this->parsed_response ? $this->parsed_response['photos']['photo'] : false;
+       }
+
+       function photos_getContext ($photo_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.getContext.html */
+               $this->request("flickr.photos.getContext", array("photo_id"=>$photo_id));
+               return $this->parsed_response ? $this->parsed_response : false;
+       }
+
+       function photos_getCounts ($dates = NULL, $taken_dates = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.getCounts.html */
+               $this->request("flickr.photos.getCounts", array("dates"=>$dates, "taken_dates"=>$taken_dates));
+               return $this->parsed_response ? $this->parsed_response['photocounts']['photocount'] : false;
+       }
+
+       function photos_getExif ($photo_id, $secret = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.getExif.html */
+               $this->request("flickr.photos.getExif", array("photo_id"=>$photo_id, "secret"=>$secret));
+               return $this->parsed_response ? $this->parsed_response['photo'] : false;
+       }
+       
+       function photos_getFavorites($photo_id, $page = NULL, $per_page = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.getFavorites.html */
+               $this->request("flickr.photos.getFavorites", array("photo_id"=>$photo_id, "page"=>$page, "per_page"=>$per_page));
+               return $this->parsed_response ? $this->parsed_response['photo'] : false;
+       }
+
+       function photos_getInfo($photo_id, $secret = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.getInfo.html */
+               $this->request("flickr.photos.getInfo", array("photo_id"=>$photo_id, "secret"=>$secret));
+               return $this->parsed_response ? $this->parsed_response['photo'] : false;
+       }
+
+       function photos_getNotInSet($min_upload_date = NULL, $max_upload_date = NULL, $min_taken_date = NULL, $max_taken_date = NULL, $privacy_filter = NULL, $extras = NULL, $per_page = NULL, $page = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.getNotInSet.html */
+               if (is_array($extras)) {
+                       $extras = implode(",", $extras);
+               }
+               $this->request("flickr.photos.getNotInSet", array("min_upload_date"=>$min_upload_date, "max_upload_date"=>$max_upload_date, "min_taken_date"=>$min_taken_date, "max_taken_date"=>$max_taken_date, "privacy_filter"=>$privacy_filter, "extras"=>$extras, "per_page"=>$per_page, "page"=>$page));
+               return $this->parsed_response ? $this->parsed_response['photos'] : false;
+       }
+
+       function photos_getPerms($photo_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.getPerms.html */
+               $this->request("flickr.photos.getPerms", array("photo_id"=>$photo_id));
+               return $this->parsed_response ? $this->parsed_response['perms'] : false;
+       }
+
+       function photos_getRecent($extras = NULL, $per_page = NULL, $page = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.getRecent.html */
+
+               if (is_array($extras)) {
+                       $extras = implode(",", $extras);
+               }
+               $this->request("flickr.photos.getRecent", array("extras"=>$extras, "per_page"=>$per_page, "page"=>$page));
+               return $this->parsed_response ? $this->parsed_response['photos'] : false;
+       }
+
+       function photos_getSizes($photo_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.getSizes.html */
+               $this->request("flickr.photos.getSizes", array("photo_id"=>$photo_id));
+               return $this->parsed_response ? $this->parsed_response['sizes']['size'] : false;
+       }
+
+       function photos_getUntagged($min_upload_date = NULL, $max_upload_date = NULL, $min_taken_date = NULL, $max_taken_date = NULL, $privacy_filter = NULL, $extras = NULL, $per_page = NULL, $page = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.getUntagged.html */
+               if (is_array($extras)) {
+                       $extras = implode(",", $extras);
+               }
+               $this->request("flickr.photos.getUntagged", array("min_upload_date"=>$min_upload_date, "max_upload_date"=>$max_upload_date, "min_taken_date"=>$min_taken_date, "max_taken_date"=>$max_taken_date, "privacy_filter"=>$privacy_filter, "extras"=>$extras, "per_page"=>$per_page, "page"=>$page));
+               return $this->parsed_response ? $this->parsed_response['photos'] : false;
+       }
+
+       function photos_getWithGeoData($args = NULL) {
+               /* See the documentation included with the photos_search() function.
+                * I'm using the same style of arguments for this function. The only
+                * difference here is that this doesn't require any arguments. The
+                * flickr.photos.search method requires at least one search parameter.
+                */
+               /* http://www.flickr.com/services/api/flickr.photos.getWithGeoData.html */
+               if (is_null($args)) {
+                       $args = array();
+               }
+               $this->request("flickr.photos.getWithGeoData", $args);
+               return $this->parsed_response ? $this->parsed_response['photos'] : false;
+       }
+
+       function photos_getWithoutGeoData($args = NULL) {
+               /* See the documentation included with the photos_search() function.
+                * I'm using the same style of arguments for this function. The only
+                * difference here is that this doesn't require any arguments. The
+                * flickr.photos.search method requires at least one search parameter.
+                */
+               /* http://www.flickr.com/services/api/flickr.photos.getWithoutGeoData.html */
+               if (is_null($args)) {
+                       $args = array();
+               }
+               $this->request("flickr.photos.getWithoutGeoData", $args);
+               return $this->parsed_response ? $this->parsed_response['photos'] : false;
+       }
+
+       function photos_recentlyUpdated($min_date = NULL, $extras = NULL, $per_page = NULL, $page = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.getUntagged.html */
+               if (is_array($extras)) {
+                       $extras = implode(",", $extras);
+               }
+               $this->request("flickr.photos.recentlyUpdated", array("min_date"=>$min_date, "extras"=>$extras, "per_page"=>$per_page, "page"=>$page));
+               return $this->parsed_response ? $this->parsed_response['photos'] : false;
+       }
+
+       function photos_removeTag($tag_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.removeTag.html */
+               $this->request("flickr.photos.removeTag", array("tag_id"=>$tag_id), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       function photos_search($args)
+       {
+               /* This function strays from the method of arguments that I've
+                * used in the other functions for the fact that there are just
+                * so many arguments to this API method. What you'll need to do
+                * is pass an associative array to the function containing the
+                * arguments you want to pass to the API.  For example:
+                *   $photos = $f->photos_search(array("tags"=>"brown,cow", "tag_mode"=>"any"));
+                * This will return photos tagged with either "brown" or "cow"
+                * or both. See the API documentation (link below) for a full
+                * list of arguments.
+                */
+
+               /* http://www.flickr.com/services/api/flickr.photos.search.html */
+               $this->request("flickr.photos.search", $args);
+               return $this->parsed_response ? $this->parsed_response['photos'] : false;
+       }
+
+       function photos_setContentType ($photo_id, $content_type) {
+               /* http://www.flickr.com/services/api/flickr.photos.setContentType.html */
+               return $this->call('flickr.photos.setContentType', array('photo_id' => $photo_id, 'content_type' => $content_type));
+       }
+       
+       function photos_setDates($photo_id, $date_posted = NULL, $date_taken = NULL, $date_taken_granularity = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.setDates.html */
+               $this->request("flickr.photos.setDates", array("photo_id"=>$photo_id, "date_posted"=>$date_posted, "date_taken"=>$date_taken, "date_taken_granularity"=>$date_taken_granularity), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       function photos_setMeta($photo_id, $title, $description)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.setMeta.html */
+               $this->request("flickr.photos.setMeta", array("photo_id"=>$photo_id, "title"=>$title, "description"=>$description), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       function photos_setPerms($photo_id, $is_public, $is_friend, $is_family, $perm_comment, $perm_addmeta)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.setPerms.html */
+               $this->request("flickr.photos.setPerms", array("photo_id"=>$photo_id, "is_public"=>$is_public, "is_friend"=>$is_friend, "is_family"=>$is_family, "perm_comment"=>$perm_comment, "perm_addmeta"=>$perm_addmeta), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       function photos_setSafetyLevel ($photo_id, $safety_level, $hidden = null) {
+               /* http://www.flickr.com/services/api/flickr.photos.setSafetyLevel.html */
+               return $this->call('flickr.photos.setSafetyLevel', array('photo_id' => $photo_id, 'safety_level' => $safety_level, 'hidden' => $hidden));
+       }
+       
+
+       function photos_setTags($photo_id, $tags)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.setTags.html */
+               $this->request("flickr.photos.setTags", array("photo_id"=>$photo_id, "tags"=>$tags), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       /* Photos - Comments Methods */
+       function photos_comments_addComment($photo_id, $comment_text) {
+               /* http://www.flickr.com/services/api/flickr.photos.comments.addComment.html */
+               $this->request("flickr.photos.comments.addComment", array("photo_id" => $photo_id, "comment_text"=>$comment_text), TRUE);
+               return $this->parsed_response ? $this->parsed_response['comment'] : false;
+       }
+
+       function photos_comments_deleteComment($comment_id) {
+               /* http://www.flickr.com/services/api/flickr.photos.comments.deleteComment.html */
+               $this->request("flickr.photos.comments.deleteComment", array("comment_id" => $comment_id), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       function photos_comments_editComment($comment_id, $comment_text) {
+               /* http://www.flickr.com/services/api/flickr.photos.comments.editComment.html */
+               $this->request("flickr.photos.comments.editComment", array("comment_id" => $comment_id, "comment_text"=>$comment_text), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       function photos_comments_getList($photo_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.comments.getList.html */
+               $this->request("flickr.photos.comments.getList", array("photo_id"=>$photo_id));
+               return $this->parsed_response ? $this->parsed_response['comments'] : false;
+       }
+
+       /* Photos - Geo Methods */
+       function photos_geo_getLocation($photo_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.geo.getLocation.html */
+               $this->request("flickr.photos.geo.getLocation", array("photo_id"=>$photo_id));
+               return $this->parsed_response ? $this->parsed_response['photo'] : false;
+       }
+
+       function photos_geo_getPerms($photo_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.geo.getPerms.html */
+               $this->request("flickr.photos.geo.getPerms", array("photo_id"=>$photo_id));
+               return $this->parsed_response ? $this->parsed_response['perms'] : false;
+       }
+
+       function photos_geo_removeLocation($photo_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.geo.removeLocation.html */
+               $this->request("flickr.photos.geo.removeLocation", array("photo_id"=>$photo_id), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       function photos_geo_setLocation($photo_id, $lat, $lon, $accuracy = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.geo.setLocation.html */
+               $this->request("flickr.photos.geo.setLocation", array("photo_id"=>$photo_id, "lat"=>$lat, "lon"=>$lon, "accuracy"=>$accuracy), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       function photos_geo_setPerms($photo_id, $is_public, $is_contact, $is_friend, $is_family)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.geo.setPerms.html */
+               $this->request("flickr.photos.geo.setPerms", array("photo_id"=>$photo_id, "is_public"=>$is_public, "is_contact"=>$is_contact, "is_friend"=>$is_friend, "is_family"=>$is_family), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       /* Photos - Licenses Methods */
+       function photos_licenses_getInfo()
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.licenses.getInfo.html */
+               $this->request("flickr.photos.licenses.getInfo");
+               return $this->parsed_response ? $this->parsed_response['licenses']['license'] : false;
+       }
+
+       function photos_licenses_setLicense($photo_id, $license_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.licenses.setLicense.html */
+               /* Requires Authentication */
+               $this->request("flickr.photos.licenses.setLicense", array("photo_id"=>$photo_id, "license_id"=>$license_id), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       /* Photos - Notes Methods */
+       function photos_notes_add($photo_id, $note_x, $note_y, $note_w, $note_h, $note_text)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.notes.add.html */
+               $this->request("flickr.photos.notes.add", array("photo_id" => $photo_id, "note_x" => $note_x, "note_y" => $note_y, "note_w" => $note_w, "note_h" => $note_h, "note_text" => $note_text), TRUE);
+               return $this->parsed_response ? $this->parsed_response['note'] : false;
+       }
+
+       function photos_notes_delete($note_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.notes.delete.html */
+               $this->request("flickr.photos.notes.delete", array("note_id" => $note_id), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       function photos_notes_edit($note_id, $note_x, $note_y, $note_w, $note_h, $note_text)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.notes.edit.html */
+               $this->request("flickr.photos.notes.edit", array("note_id" => $note_id, "note_x" => $note_x, "note_y" => $note_y, "note_w" => $note_w, "note_h" => $note_h, "note_text" => $note_text), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       /* Photos - Transform Methods */
+       function photos_transform_rotate($photo_id, $degrees)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.transform.rotate.html */
+               $this->request("flickr.photos.transform.rotate", array("photo_id" => $photo_id, "degrees" => $degrees), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       /* Photos - Upload Methods */
+       function photos_upload_checkTickets($tickets)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.upload.checkTickets.html */
+               if (is_array($tickets)) {
+                       $tickets = implode(",", $tickets);
+               }
+               $this->request("flickr.photos.upload.checkTickets", array("tickets" => $tickets), TRUE);
+               return $this->parsed_response ? $this->parsed_response['uploader']['ticket'] : false;
+       }
+
+       /* Photosets Methods */
+       function photosets_addPhoto($photoset_id, $photo_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.photosets.addPhoto.html */
+               $this->request("flickr.photosets.addPhoto", array("photoset_id" => $photoset_id, "photo_id" => $photo_id), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       function photosets_create($title, $description, $primary_photo_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.photosets.create.html */
+               $this->request("flickr.photosets.create", array("title" => $title, "primary_photo_id" => $primary_photo_id, "description" => $description), TRUE);
+               return $this->parsed_response ? $this->parsed_response['photoset'] : false;
+       }
+
+       function photosets_delete($photoset_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.photosets.delete.html */
+               $this->request("flickr.photosets.delete", array("photoset_id" => $photoset_id), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       function photosets_editMeta($photoset_id, $title, $description = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.photosets.editMeta.html */
+               $this->request("flickr.photosets.editMeta", array("photoset_id" => $photoset_id, "title" => $title, "description" => $description), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       function photosets_editPhotos($photoset_id, $primary_photo_id, $photo_ids)
+       {
+               /* http://www.flickr.com/services/api/flickr.photosets.editPhotos.html */
+               $this->request("flickr.photosets.editPhotos", array("photoset_id" => $photoset_id, "primary_photo_id" => $primary_photo_id, "photo_ids" => $photo_ids), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       function photosets_getContext($photo_id, $photoset_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.photosets.getContext.html */
+               $this->request("flickr.photosets.getContext", array("photo_id" => $photo_id, "photoset_id" => $photoset_id));
+               return $this->parsed_response ? $this->parsed_response : false;
+       }
+
+       function photosets_getInfo($photoset_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.photosets.getInfo.html */
+               $this->request("flickr.photosets.getInfo", array("photoset_id" => $photoset_id));
+               return $this->parsed_response ? $this->parsed_response['photoset'] : false;
+       }
+
+       function photosets_getList($user_id = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.photosets.getList.html */
+               $this->request("flickr.photosets.getList", array("user_id" => $user_id));
+               return $this->parsed_response ? $this->parsed_response['photosets'] : false;
+       }
+
+       function photosets_getPhotos($photoset_id, $extras = NULL, $privacy_filter = NULL, $per_page = NULL, $page = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.photosets.getPhotos.html */
+               $this->request("flickr.photosets.getPhotos", array("photoset_id" => $photoset_id, "extras" => $extras, "privacy_filter" => $privacy_filter, "per_page" => $per_page, "page" => $page));
+               return $this->parsed_response ? $this->parsed_response['photoset'] : false;
+       }
+
+       function photosets_orderSets($photoset_ids)
+       {
+               /* http://www.flickr.com/services/api/flickr.photosets.orderSets.html */
+               if (is_array($photoset_ids)) {
+                       $photoset_ids = implode(",", $photoset_ids);
+               }
+               $this->request("flickr.photosets.orderSets", array("photoset_ids" => $photoset_ids), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       function photosets_removePhoto($photoset_id, $photo_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.photosets.removePhoto.html */
+               $this->request("flickr.photosets.removePhoto", array("photoset_id" => $photoset_id, "photo_id" => $photo_id), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       /* Photosets Comments Methods */
+       function photosets_comments_addComment($photoset_id, $comment_text) {
+               /* http://www.flickr.com/services/api/flickr.photosets.comments.addComment.html */
+               $this->request("flickr.photosets.comments.addComment", array("photoset_id" => $photoset_id, "comment_text"=>$comment_text), TRUE);
+               return $this->parsed_response ? $this->parsed_response['comment'] : false;
+       }
+
+       function photosets_comments_deleteComment($comment_id) {
+               /* http://www.flickr.com/services/api/flickr.photosets.comments.deleteComment.html */
+               $this->request("flickr.photosets.comments.deleteComment", array("comment_id" => $comment_id), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       function photosets_comments_editComment($comment_id, $comment_text) {
+               /* http://www.flickr.com/services/api/flickr.photosets.comments.editComment.html */
+               $this->request("flickr.photosets.comments.editComment", array("comment_id" => $comment_id, "comment_text"=>$comment_text), TRUE);
+               return $this->parsed_response ? true : false;
+       }
+
+       function photosets_comments_getList($photoset_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.photosets.comments.getList.html */
+               $this->request("flickr.photosets.comments.getList", array("photoset_id"=>$photoset_id));
+               return $this->parsed_response ? $this->parsed_response['comments'] : false;
+       }
+       
+       /* Places Methods */
+       function places_resolvePlaceId ($place_id) {
+               /* http://www.flickr.com/services/api/flickr.places.resolvePlaceId.html */
+               $rsp = $this->call('flickr.places.resolvePlaceId', array('place_id' => $place_id));
+               return $rsp ? $rsp['location'] : $rsp;
+       }
+       
+       function places_resolvePlaceURL ($url) {
+               /* http://www.flickr.com/services/api/flickr.places.resolvePlaceURL.html */
+               $rsp = $this->call('flickr.places.resolvePlaceURL', array('url' => $url));
+               return $rsp ? $rsp['location'] : $rsp;
+       }
+
+       /* Prefs Methods */
+       function prefs_getContentType () {
+               /* http://www.flickr.com/services/api/flickr.prefs.getContentType.html */
+               $rsp = $this->call('flickr.prefs.getContentType', array());
+               return $rsp ? $rsp['person'] : $rsp;
+       }
+       
+       function prefs_getHidden () {
+               /* http://www.flickr.com/services/api/flickr.prefs.getHidden.html */
+               $rsp = $this->call('flickr.prefs.getHidden', array());
+               return $rsp ? $rsp['person'] : $rsp;
+       }
+       
+       function prefs_getPrivacy () {
+               /* http://www.flickr.com/services/api/flickr.prefs.getPrivacy.html */
+               $rsp = $this->call('flickr.prefs.getPrivacy', array());
+               return $rsp ? $rsp['person'] : $rsp;
+       }
+       
+       function prefs_getSafetyLevel () {
+               /* http://www.flickr.com/services/api/flickr.prefs.getSafetyLevel.html */
+               $rsp = $this->call('flickr.prefs.getSafetyLevel', array());
+               return $rsp ? $rsp['person'] : $rsp;
+       }
+
+       /* Reflection Methods */
+       function reflection_getMethodInfo($method_name)
+       {
+               /* http://www.flickr.com/services/api/flickr.reflection.getMethodInfo.html */
+               $this->request("flickr.reflection.getMethodInfo", array("method_name" => $method_name));
+               return $this->parsed_response ? $this->parsed_response : false;
+       }
+
+       function reflection_getMethods()
+       {
+               /* http://www.flickr.com/services/api/flickr.reflection.getMethods.html */
+               $this->request("flickr.reflection.getMethods");
+               return $this->parsed_response ? $this->parsed_response['methods']['method'] : false;
+       }
+
+       /* Tags Methods */
+       function tags_getHotList($period = NULL, $count = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.tags.getHotList.html */
+               $this->request("flickr.tags.getHotList", array("period" => $period, "count" => $count));
+               return $this->parsed_response ? $this->parsed_response['hottags'] : false;
+       }
+
+       function tags_getListPhoto($photo_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.tags.getListPhoto.html */
+               $this->request("flickr.tags.getListPhoto", array("photo_id" => $photo_id));
+               return $this->parsed_response ? $this->parsed_response['photo']['tags']['tag'] : false;
+       }
+
+       function tags_getListUser($user_id = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.tags.getListUser.html */
+               $this->request("flickr.tags.getListUser", array("user_id" => $user_id));
+               return $this->parsed_response ? $this->parsed_response['who']['tags']['tag'] : false;
+       }
+
+       function tags_getListUserPopular($user_id = NULL, $count = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.tags.getListUserPopular.html */
+               $this->request("flickr.tags.getListUserPopular", array("user_id" => $user_id, "count" => $count));
+               return $this->parsed_response ? $this->parsed_response['who']['tags']['tag'] : false;
+       }
+
+       function tags_getListUserRaw($tag)
+       {
+               /* http://www.flickr.com/services/api/flickr.tags.getListUserRaw.html */
+               $this->request("flickr.tags.getListUserRaw", array("tag" => $tag));
+               return $this->parsed_response ? $this->parsed_response['who']['tags']['tag'][0]['raw'] : false;
+       }
+
+       function tags_getRelated($tag)
+       {
+               /* http://www.flickr.com/services/api/flickr.tags.getRelated.html */
+               $this->request("flickr.tags.getRelated", array("tag" => $tag));
+               return $this->parsed_response ? $this->parsed_response['tags'] : false;
+       }
+
+       function test_echo($args = array())
+       {
+               /* http://www.flickr.com/services/api/flickr.test.echo.html */
+               $this->request("flickr.test.echo", $args);
+               return $this->parsed_response ? $this->parsed_response : false;
+       }
+
+       function test_login()
+       {
+               /* http://www.flickr.com/services/api/flickr.test.login.html */
+               $this->request("flickr.test.login");
+               return $this->parsed_response ? $this->parsed_response['user'] : false;
+       }
+
+       function urls_getGroup($group_id)
+       {
+               /* http://www.flickr.com/services/api/flickr.urls.getGroup.html */
+               $this->request("flickr.urls.getGroup", array("group_id"=>$group_id));
+               return $this->parsed_response ? $this->parsed_response['group']['url'] : false;
+       }
+
+       function urls_getUserPhotos($user_id = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.urls.getUserPhotos.html */
+               $this->request("flickr.urls.getUserPhotos", array("user_id"=>$user_id));
+               return $this->parsed_response ? $this->parsed_response['user']['url'] : false;
+       }
+
+       function urls_getUserProfile($user_id = NULL)
+       {
+               /* http://www.flickr.com/services/api/flickr.urls.getUserProfile.html */
+               $this->request("flickr.urls.getUserProfile", array("user_id"=>$user_id));
+               return $this->parsed_response ? $this->parsed_response['user']['url'] : false;
+       }
+
+       function urls_lookupGroup($url)
+       {
+               /* http://www.flickr.com/services/api/flickr.urls.lookupGroup.html */
+               $this->request("flickr.urls.lookupGroup", array("url"=>$url));
+               return $this->parsed_response ? $this->parsed_response['group'] : false;
+       }
+
+       function urls_lookupUser($url)
+       {
+               /* http://www.flickr.com/services/api/flickr.photos.notes.edit.html */
+               $this->request("flickr.urls.lookupUser", array("url"=>$url));
+               return $this->parsed_response ? $this->parsed_response['user'] : false;
+       }
+}
+
+
+?>
diff --git a/style.css b/style.css
new file mode 100644 (file)
index 0000000..47336bd
--- /dev/null
+++ b/style.css
@@ -0,0 +1,94 @@
+body {
+    background-color:#aaaaaa;
+    padding:0px;
+    margin:0px;
+    font-family:verdana,arial,helvetica,sans-serif;
+    font-size:10px;
+    color:black;
+}
+
+div.component {
+    position:absolute;
+    border:1px solid black;
+    background-color:white;
+    margin:0px;
+}
+
+p, span {
+    margin:8px;
+}
+
+ul {
+    margin-left: 0px;
+    padding-left: 0px;
+}
+
+li {
+    margin-left: 0px;
+    list-style: none;
+    margin:8px;
+}
+
+li.last {
+    margin-top:20px;
+    text-align:right;
+}
+
+h2 {
+    margin-left:14px;
+    font-size:14px;
+    color:#336699;
+    border-bottom:1px dashed #aaaaaa;
+}
+
+a {
+    color:#888888;
+    text-decoration:none;
+}
+
+a:hover {
+    color:#336699;
+    text-decoration:underline;
+}
+
+.mind {
+    border:1px dashed #aaaaaa;
+    padding:5px;
+    display:block;
+}
+
+div#flickr {
+    left:20px;
+    top:20px;
+    padding:5px;
+    width:770px;
+    text-align:center;
+}
+div#flickr img {
+    margin:5px;
+}
+
+div#main {
+    left:20px;
+    width:780px;
+    height:195px;
+    top:135px;
+}
+
+div#blog {
+    top:350px;
+    left:20px;
+    width:220px;
+}
+
+div#twitter {
+    top:350px;
+    left:260px;
+    width:300px;
+}
+
+div#lastfm{
+    top:350px;
+    left:580px;
+    width:220px;
+}