From b58a57ba0b3beca67d2d94d5c02633df780cecd1 Mon Sep 17 00:00:00 2001 From: Penny Leach Date: Sat, 26 Apr 2008 00:37:44 +1200 Subject: [PATCH 1/1] initial checkin --- .gitignore | 1 + comp/blog.php | 15 + comp/flickr.php | 22 + comp/lastfm.php | 14 + comp/main.php | 5 + comp/twitter.php | 15 + config-dist.php | 21 + include/main.inc | 29 + index.php | 26 + lib/Cache/Lite.php | 826 ++++++++++++++ lib/Cache/Lite/File.php | 92 ++ lib/Cache/Lite/Function.php | 211 ++++ lib/Cache/Lite/Output.php | 72 ++ lib/HTTP/Request.php | 1484 ++++++++++++++++++++++++++ lib/HTTP/Request/Listener.php | 106 ++ lib/Net/Socket.php | 528 +++++++++ lib/Net/URL.php | 410 +++++++ lib/PEAR.php | 1108 +++++++++++++++++++ lib/PEAR/Exception.php | 397 +++++++ lib/Snoopy.class.php | 1257 ++++++++++++++++++++++ lib/XML/Feed/Parser.php | 351 ++++++ lib/XML/Feed/Parser/Atom.php | 365 +++++++ lib/XML/Feed/Parser/AtomElement.php | 261 +++++ lib/XML/Feed/Parser/Exception.php | 42 + lib/XML/Feed/Parser/RSS09.php | 214 ++++ lib/XML/Feed/Parser/RSS09Element.php | 62 ++ lib/XML/Feed/Parser/RSS1.php | 277 +++++ lib/XML/Feed/Parser/RSS11.php | 276 +++++ lib/XML/Feed/Parser/RSS11Element.php | 151 +++ lib/XML/Feed/Parser/RSS1Element.php | 116 ++ lib/XML/Feed/Parser/RSS2.php | 334 ++++++ lib/XML/Feed/Parser/RSS2Element.php | 171 +++ lib/XML/Feed/Parser/Type.php | 441 ++++++++ lib/general.php | 91 ++ lib/phpFlickr.php | 1414 ++++++++++++++++++++++++ style.css | 94 ++ 36 files changed, 11299 insertions(+) create mode 100644 .gitignore create mode 100644 comp/blog.php create mode 100644 comp/flickr.php create mode 100644 comp/lastfm.php create mode 100644 comp/main.php create mode 100644 comp/twitter.php create mode 100644 config-dist.php create mode 100644 include/main.inc create mode 100644 index.php create mode 100644 lib/Cache/Lite.php create mode 100644 lib/Cache/Lite/File.php create mode 100644 lib/Cache/Lite/Function.php create mode 100644 lib/Cache/Lite/Output.php create mode 100644 lib/HTTP/Request.php create mode 100644 lib/HTTP/Request/Listener.php create mode 100644 lib/Net/Socket.php create mode 100644 lib/Net/URL.php create mode 100644 lib/PEAR.php create mode 100644 lib/PEAR/Exception.php create mode 100644 lib/Snoopy.class.php create mode 100644 lib/XML/Feed/Parser.php create mode 100644 lib/XML/Feed/Parser/Atom.php create mode 100644 lib/XML/Feed/Parser/AtomElement.php create mode 100644 lib/XML/Feed/Parser/Exception.php create mode 100644 lib/XML/Feed/Parser/RSS09.php create mode 100644 lib/XML/Feed/Parser/RSS09Element.php create mode 100644 lib/XML/Feed/Parser/RSS1.php create mode 100644 lib/XML/Feed/Parser/RSS11.php create mode 100644 lib/XML/Feed/Parser/RSS11Element.php create mode 100644 lib/XML/Feed/Parser/RSS1Element.php create mode 100644 lib/XML/Feed/Parser/RSS2.php create mode 100644 lib/XML/Feed/Parser/RSS2Element.php create mode 100644 lib/XML/Feed/Parser/Type.php create mode 100644 lib/general.php create mode 100644 lib/phpFlickr.php create mode 100644 style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b74decb --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/config.php diff --git a/comp/blog.php b/comp/blog.php new file mode 100644 index 0000000..dd39326 --- /dev/null +++ b/comp/blog.php @@ -0,0 +1,15 @@ +latest blog posts'; + if ($data = parse_rss(get_config('blog_rss'))) { + echo ''; + } +} + +?> diff --git a/comp/flickr.php b/comp/flickr.php new file mode 100644 index 0000000..a0d9595 --- /dev/null +++ b/comp/flickr.php @@ -0,0 +1,22 @@ +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 ""; + echo "$photo[title]"; + echo ""; + $i++; + } +} diff --git a/comp/lastfm.php b/comp/lastfm.php new file mode 100644 index 0000000..5435e20 --- /dev/null +++ b/comp/lastfm.php @@ -0,0 +1,14 @@ +recently listened tracks'; + if ($data = parse_rss('http://ws.audioscrobbler.com/1.0/user/' . get_config('lastfm_username') . '/recenttracks.rss')) { + echo ''; + } + +} diff --git a/comp/main.php b/comp/main.php new file mode 100644 index 0000000..61e1946 --- /dev/null +++ b/comp/main.php @@ -0,0 +1,5 @@ +latest twitter updates'; + if ($data = parse_rss('http://twitter.com/statuses/user_timeline/' . get_config('twitter_username') . '.rss')) { + echo ''; + } +} + +?> diff --git a/config-dist.php b/config-dist.php new file mode 100644 index 0000000..9bd5005 --- /dev/null +++ b/config-dist.php @@ -0,0 +1,21 @@ + '', + '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 index 0000000..6f23673 --- /dev/null +++ b/include/main.inc @@ -0,0 +1,29 @@ + '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', +); + +?> +

mjollnir.org

+

a collection of what I'm doing all over the internet, mashed together into one place for your viewing pleasure.

+

my blog lives at she.geek.nz. there's an about page there too. it's old and rather out of date.

+

find me on $url) { + echo '' . $service . ''; + if ($count != count($whorepile) -1) { + echo ', '; + } + $count++; + } +?>. christ. at least I never got livejournal.

+

I don't have an rss feed for this mashup. use friendfeed instead, or an individual site's feed.

+

thanks to vex for the vserver

+

send me email! +On my mind: mahara, more tattoos, less tattoos, moodle gsoc, Europe diff --git a/index.php b/index.php new file mode 100644 index 0000000..97e7f50 --- /dev/null +++ b/index.php @@ -0,0 +1,26 @@ +'; +?> + + + + mjollnir.org + + + + +'; + require_once(get_config('comp') . $comp . '.php'); + call_user_func('comp_' . $comp); + echo ' '; + } +?> + + diff --git a/lib/Cache/Lite.php b/lib/Cache/Lite.php new file mode 100644 index 0000000..335b15d --- /dev/null +++ b/lib/Cache/Lite.php @@ -0,0 +1,826 @@ + +* +* Nota : A chinese documentation (thanks to RainX ) 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 +*/ + +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) { + 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) { + $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) { + $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 index 0000000..f048acf --- /dev/null +++ b/lib/Cache/Lite/File.php @@ -0,0 +1,92 @@ + +*/ + +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 index 0000000..63a96d9 --- /dev/null +++ b/lib/Cache/Lite/Function.php @@ -0,0 +1,211 @@ + +* @author Fabien MARTY +*/ + +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 index 0000000..9732273 --- /dev/null +++ b/lib/Cache/Lite/Output.php @@ -0,0 +1,72 @@ + +*/ + +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 index 0000000..55e227b --- /dev/null +++ b/lib/HTTP/Request.php @@ -0,0 +1,1484 @@ + + * @author Alexey Borzov + * @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): + * + * $a = &new HTTP_Request('http://www.yahoo.com/'); + * $a->sendRequest(); + * echo $a->getResponseBody(); + * + * + * @category HTTP + * @package HTTP_Request + * @author Richard Heyes + * @author Alexey Borzov + * @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: + *

+ * @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 + * @author Alexey Borzov + * @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 index 0000000..b005142 --- /dev/null +++ b/lib/HTTP/Request/Listener.php @@ -0,0 +1,106 @@ + + * @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 + * @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 index 0000000..96f2c4b --- /dev/null +++ b/lib/Net/Socket.php @@ -0,0 +1,528 @@ + | +// | Chuck Hagenbuch | +// +----------------------------------------------------------------------+ +// +// $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 + * @author Chuck Hagenbuch + */ +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: + * + *

+ * timed_out (bool) - The socket timed out waiting for data
+ * blocked (bool) - The socket was blocked
+ * eof (bool) - Indicates EOF event
+ * unread_bytes (int) - Number of bytes left in the socket buffer
+ *

+ * + * @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 index 0000000..38e26fd --- /dev/null +++ b/lib/Net/URL.php @@ -0,0 +1,410 @@ + | +// +-----------------------------------------------------------------------+ +// +// $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 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 + */ + 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 index 0000000..e442d8c --- /dev/null +++ b/lib/PEAR.php @@ -0,0 +1,1108 @@ + + * @author Stig Bakken + * @author Tomas V.V.Cox + * @author Greg Beaver + * @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 + * @author Tomas V.V. Cox + * @author Greg Beaver + * @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 + * @author Tomas V.V. Cox + * @author Gregory Beaver + * @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 index 0000000..b8bba0e --- /dev/null +++ b/lib/PEAR/Exception.php @@ -0,0 +1,397 @@ + + * @author Hans Lellelid + * @author Bertrand Mansion + * @author Greg Beaver + * @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 + * + * + * 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; + * } + * + * + * @category pear + * @package PEAR + * @author Tomas V.V.Cox + * @author Hans Lellelid + * @author Bertrand Mansion + * @author Greg Beaver + * @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: + *
+     * array('name' => $name, 'context' => array(...))
+     * 
+ * @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 = '' . "\n"; + foreach ($causes as $i => $cause) { + $html .= '\n"; + } + $html .= '' . "\n" + . '' + . '' + . '' . "\n"; + + foreach ($trace as $k => $v) { + $html .= '' + . '' + . '' . "\n"; + } + $html .= '' + . '' + . '' . "\n" + . '
' + . str_repeat('-', $i) . ' ' . $cause['class'] . ': ' + . htmlspecialchars($cause['message']) . ' in ' . $cause['file'] . ' ' + . 'on line ' . $cause['line'] . '' + . "
Exception trace
#FunctionLocation
' . $k . ''; + 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 .= '…'; + $args[] = "'" . $str . "'"; + } + } + } + $html .= '(' . implode(', ',$args) . ')' + . '' . (isset($v['file']) ? $v['file'] : 'unknown') + . ':' . (isset($v['line']) ? $v['line'] : 'unknown') + . '
' . ($k+1) . '{main} 
'; + 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 index 0000000..77e5b73 --- /dev/null +++ b/lib/Snoopy.class.php @@ -0,0 +1,1257 @@ + +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;$xresults);$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;$xresults);$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;$xresults);$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;$xresults);$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;$xresults);$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 ]+)) # 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("']*?>.*?'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);'", + "'ß'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("']*?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("']+)'i",$results,$match)) + { + $this->results[] = $results; + for($x=0; $x_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("']*?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("']+)'i",$results,$match)) + { + $this->results[] = $results; + for($x=0; $x_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; $xheaders); $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 index 0000000..67668c4 --- /dev/null +++ b/lib/XML/Feed/Parser.php @@ -0,0 +1,351 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 index 0000000..9b70bea --- /dev/null +++ b/lib/XML/Feed/Parser/Atom.php @@ -0,0 +1,365 @@ + + * @copyright 2005 James Stewart + * @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 + * @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('<', '<', $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 index 0000000..d8a39fe --- /dev/null +++ b/lib/XML/Feed/Parser/AtomElement.php @@ -0,0 +1,261 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 index 0000000..abc034c --- /dev/null +++ b/lib/XML/Feed/Parser/Exception.php @@ -0,0 +1,42 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 index 0000000..7004e01 --- /dev/null +++ b/lib/XML/Feed/Parser/RSS09.php @@ -0,0 +1,214 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 index 0000000..6ec8c8d --- /dev/null +++ b/lib/XML/Feed/Parser/RSS09Element.php @@ -0,0 +1,62 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 index 0000000..952f60e --- /dev/null +++ b/lib/XML/Feed/Parser/RSS1.php @@ -0,0 +1,277 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 index 0000000..74babc1 --- /dev/null +++ b/lib/XML/Feed/Parser/RSS11.php @@ -0,0 +1,276 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 index 0000000..9de6417 --- /dev/null +++ b/lib/XML/Feed/Parser/RSS11Element.php @@ -0,0 +1,151 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 index 0000000..f43ae46 --- /dev/null +++ b/lib/XML/Feed/Parser/RSS1Element.php @@ -0,0 +1,116 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 index 0000000..c5d79d1 --- /dev/null +++ b/lib/XML/Feed/Parser/RSS2.php @@ -0,0 +1,334 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 index 0000000..156e9bc --- /dev/null +++ b/lib/XML/Feed/Parser/RSS2Element.php @@ -0,0 +1,171 @@ + + * @copyright 2005 James Stewart + * @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 + * @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 index 0000000..9ccf48b --- /dev/null +++ b/lib/XML/Feed/Parser/Type.php @@ -0,0 +1,441 @@ + + * @copyright 2005 James Stewart + * @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 + * @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: + * PEAR and: + * + * 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 .= '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 index 0000000..fab6aa5 --- /dev/null +++ b/lib/general.php @@ -0,0 +1,91 @@ + 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 index 0000000..f1c194c --- /dev/null +++ b/lib/phpFlickr.php @@ -0,0 +1,1414 @@ +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('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("(.*)", $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('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("(.*)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('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 . ">(.*)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 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; +} -- 2.39.5