<?php
/*

see for ideas
http://www.hackdiary.com/archives/000060.html

0.1 - 10/11/04
- initial release

0.2 - 10/15/04
- sends user-agent
- improved error handling, added error codes
- added HTTP header parsing and handling
- now disables all calls after user is throttled (receive http status 503)
- added support for /all method with get_all_posts()
- fixed the delete_post method

0.3 - future
- TODO - cookie support
- TODO - implement support for http 1.1 persistent connections
- TODO - caching

*/

// error codes
define('DEL_ERR_NOCREDENTIALS',1);
define('DEL_ERR_THROTTLED',2);
define('DEL_ERR_MANGLEDHEADERS',3);
define('DEL_ERR_SOCKETCONNECT',4);
define('DEL_ERR_SOCKETWRITE',5);
define('DEL_ERR_XMLPARSE',6);

class delicious
{
	var $title = 'Delicious-x-4';
	var $version = '0.2';
	var $host = 'del.icio.us';
	var $port = 80;
	var $username = false;
	var $password = false;
	var $error = false; // is set to an error code upon any error
	var $debug_flag = 0; // 1 = append to to $del->debug_str, 2 = echo() out line by line
	var $debug_str = "";
	var $throttled = false; // set to true if server returns 503, deny any future requests
	var $incoming_headers = array();
	var $request = "";
	var $response = "";
	var $cookie = false;
	
	// constructor
	function delicious($username,$password)
	{
		$this->username = $username;
		$this->password = $password;
	}
	
	// send http message, return parsed response
	function send($path)
	{
		// throttle check
		if($this->throttled)
		{
			$this->setError(DEL_ERR_THROTTLED);
			return false;
		}
		
		// check that credentials have been set
		if(!$this->username || !$this->password)
		{
			$this->setError(DEL_ERR_NOCREDENTIALS);
			return false;
		}
		
		// build http request message
		$msg =
		"GET http://".$this->host."$path HTTP/1.0\r\n"
		.'User-Agent: '.$this->title.'/'.$this->version."\r\n";
		
		// cookie or credentials
		$msg .= 'Authorization: Basic '.base64_encode($this->username.':'.$this->password)."\r\n";
		/*
		if(!$this->cookie)
		{
			$msg .= 'Authorization: Basic '.base64_encode($this->username.':'.$this->password)."\r\n";
		}
		else
		{
			$msg .= 'Cookie:'.$this->cookie."\r\n";
		}
		*/
		
		// close message
		$msg .= "\r\n";
		
		$this->request = $msg;
		//$this->debug('REQUEST:');
		//$this->debug('<xmp>'.$msg.'</xmp>');
		
		// make call
		if( $fp = pfsockopen( $this->host, $this->port) )
		{
			if( fwrite($fp, $msg) )
			{
				$response = '';
				while (!feof($fp))
				{
					$response .= fread($fp, 8192);
				}
				
				fclose($fp);
				
				$this->response = $response;
				//$this->debug('RESPONSE:');
				//$this->debug('<xmp>'.$response.'</xmp>');
				
				// parse response message
				$pos = strpos($response,"\r\n\r\n");
				if($pos < 1)
				{
					$pos = strpos($data,"\n\n");
				}
				
				// separate headers and body
				if($headers = $this->parseHeaders(trim(substr($response,0,$pos))))
				{
					foreach($headers as $k => $v)
					{
						$this->debug("HTTP Response Header: $k -> $v");
					}
					
					// mangled headers?
					if(!isset($headers['status']))
					{
						$this->debug('send(): no status in headers');
						$this->setError(DEL_ERR_MANGLEDHEADERS);
						return false;
					}
					
					// handle 503
					if($headers['status'] == 503)
					{
						$this->throttled = true;
						$this->setError(DEL_ERR_THROTTLED);
						return false;
					}
					
					// process cookie
					if(isset($headers['set-cookie']))
					{
						$this->cookie = substr( $headers['set-cookie'], 0, strpos($headers['set-cookie'],';') ).';';
						$this->debug('send(): setting cookie: '.$this->cookie);
					}
					
					// handle unchanged
					
					// get xml response
					$xml = trim(substr($response,$pos));
					
					// if xml response
					if(strlen($xml) > 0)
					{
						$this->debug('send(): parsing xml');
						// parse into assoc array and return
						if($result = $this->parseResponse($xml))
						{
							return $result;
						}
						else
						{
							$this->debug('got parser error');
							$this->setError(DEL_ERROR_XMLPARSE);
						}
					}
					else
					{
						return array();
					}
				}
				else
				{
					// no proper headers found - mangled response?
					$this->setError(DEL_ERR_MANGLEDHEADERS);
					return false;
				}
			}
			else
			{
				$this->setError(DEL_ERR_SOCKETWRITE);
				return false;
			}
		}
		else
		{
			$this->setError(DEL_ERR_SOCKETCONNECT);
			return false;
		}
	}
	
	// parse headers
	function parseHeaders($str)
	{
		//$this->debug("parseHeaders(): header string: $str");
		// find linebreak type
		$lb = false;
		$pos = strpos($str,"\r\n");
		if($pos > 1){
			$lb = "\r\n";
		} else {
			$pos = strpos($str,"\n");
			if($pos > 1){
				$lb = "\n";
			}
		}
		
		// if linebreak is found
		if(!$lb)
		{
			$this->debug('parseHeaders(): no linebreak found');
			return false;
		}
		else
		{
			$header_array = explode( $lb, $str );
			foreach($header_array as $header_line){
				$arr = explode(':',$header_line, 2);
				if(count($arr) > 1){
					$header_name = strtolower(trim($arr[0]));
					$this->incoming_headers[$header_name] = trim($arr[1]);
				} else if (isset($header_name)) {
					// append continuation line to previous header
					$this->incoming_headers[$header_name] .= $lb . ' ' . $header_line;
				} elseif ( strstr($header_line,'HTTP')) {
					$arr = explode(' ', $header_line);
					$this->incoming_headers['status'] = $arr[1];
				}
			}
			return $this->incoming_headers;
		}
	}
	
	// parse response
	function parseResponse($xml)
	{
		$parser = xml_parser_create('');
		xml_parser_set_option($parser,XML_OPTION_CASE_FOLDING,0);
		xml_parser_set_option($parser,XML_OPTION_SKIP_WHITE,1);
		$return = xml_parse_into_struct($parser,$xml,$values,$tags);
		
		//$this->debug( 'parseResponse(): xml_parse_into_struct return: '.$return);
		
		// handle errors
		if($return == 0)
		{
			$this->debug( 'parseResponse(): xml error - '.xml_error_string( xml_get_error_code($parser) ) );
			return false;
		}
		else
		{
			//$this->debug( $this->htmlvar_dump($tags) );
			//$this->debug( $this->htmlvar_dump($values) );
			
			foreach($tags as $tagname => $indices)
			{
				foreach($indices as $index)
				{
					$id = uniqid('');
					
					if($values[$index]['type'] != 'close')
					{
						// store element details
						$content[$id] = $values[$index];
						
						$tagname = $values[$index]['tag'];
						
						// set self to value of last open tag flag
						if($values[$index]['type'] == 'open')
						{
							$lastOpenTagAtLevel[ $values[$index]['level'] ] = $id;
						}
						
						// get parent id
						$myParent = $lastOpenTagAtLevel[ ($values[$index]['level']-1) ];
						
						// if root, flag, and don't add to parent
						if($values[$index]['level'] == 1)
						{
							$root = $id;
						}
						// if complete, add self to parent result
						elseif($values[$index]['type'] == 'complete')
						{
							$content[ $myParent ]['result'][$tagname][] = $values[$index]['attributes'];
						}
						// if open tag, add reference to my result to parents result
						elseif($values[$index]['type'] == 'open')
						{
							$content[ $myParent ]['result'][$tagname][] = &$values[$index]['result'];
						}
					}
				}
			}
			
			// handle case where xml document is a single complete tag w/ no children
			if(!isset($content[$root]['result']))
			{
				$result = $content[$root]['attributes'];
			}
			else
			{
				$result = array_shift($content[$root]['result']);
			}
			//$this->debug( $this->htmlvar_dump($result) );
			
			xml_parser_free($parser);
			
			return $result;
		}
	}
	
	// set errors
	function setError($str)
	{
		$this->error = $str;
	}
	
	// get errors
	function getError()
	{
		return $this->error;
	}
	
	// handle debug output
	function debug($str)
	{
		if($this->debug_flag == 1)
		{
			$this->debug_str .= $str."\n";
		}
		elseif($this->debug_flag == 2)
		{
			echo $str.'<br>';
		}
	}
	
	function htmlvar_dump($var)
	{
		ob_start();
		var_dump($var);
		$str = ob_get_contents();
		ob_end_clean();
		return '<pre>'.$str.'</pre>';
	}
	/*
	function: http://del.icio.us/api/posts/dates?
	&tag= filter by this tag - optional
	returns a list of dates with the number of posts at each date. 
	[tag] adds a filter
	*/
	function get_post_dates($tag=false)
	{
		// build path and query string
		$queryString =
		'/api/posts/dates?'; // action
		
		if($tag)
		{
			$queryString .= '&tag='.urlencode($tag);
		}
		
		// send
		return $this->send($queryString);
	}
	
	/*
	function: http://del.icio.us/api/tags/get?
	returns a list of tags the user has used.
	*/
	function get_tags()
	{
		// build path and query string
		$queryString =
		'/api/tags/get?'; // action
		
		// send
		return $this->send($queryString);
	}
	
	/*
	function: http://del.icio.us/api/posts/get?
		&tag= filter by this tag - optional
		&dt= filter by this date
	returns a list of posts on a given date, filtered by tag. if no 
	date is supplied, most recent date will be used
	*/
	function get_posts($tag=false,$dt=false)
	{
		// build path and query string
		$queryString =
		'/api/posts/get?'; // action
		
		if($tag)
		{
			$queryString .= '&tag='.urlencode($tag);
		}
		
		if($dt)
		{
			$queryString .= '&dt='.urlencode($dt);
		}
		
		// send
		return $this->send($queryString);
	}
	
	/*
	function: http://del.icio.us/api/posts/recent?
		&tag= filter by this tag - optional
		&count= number of items to retrieve - optional (defaults to 15)
	returns a list of most recent posts, possibly filtered by tag.
	*/
	function get_recent_posts($tag=false,$count=false)
	{
		// build path and query string
		$queryString =
		'/api/posts/recent?'; // action
		
		if($tag)
		{
			$queryString .= '&tag='.urlencode($tag);
		}
		
		if($count)
		{
			$queryString .= '&count='.urlencode($count);
		}
		
		// send
		return $this->send($queryString);
	}
	
	/*
	function: http://del.icio.us/api/posts/all
	returns all posts. use sparingly.
	*/
	function get_all_posts()
	{
		// build path and query string
		$queryString = '/api/posts/all'; // action
		
		// send
		return $this->send($queryString);
	}
	
	/*
	function: http://del.icio.us/api/posts/add?
		&url= url for post
		&description= description for post
		&extended= extended for post
		&tags= space-delimited list of tags
		&dt= datestamp for post, format "CCYY-MM-DDThh:mm:ssZ"
	makes a post to delicious
	*/
	function add_post($link, $title, $content, $category, $date)
	{
		// prep data
		//$date = date('Y-m-d\TH:i:s\Z', strtotime($date) );
		
		// build path and query string
		$queryString =
		'/api/posts/add?'					// action
		.'&url='.$link						// link for post
		.'&description='.urlencode($title)	// description for post
		.'&extended='.urlencode($content)	// extended for post
		.'&tags='.urlencode($category) 		// space-delimited list of tags
		.'&dt='.urlencode($date); 			// datestamp for post, format "CCYY-MM-DDThh:mm:ssZ"
		
		// send
		return $this->send($queryString);
	}
	
	/*
	function: http://del.icio.us/api/tags/rename?
		&old= old tag
		&new= new tag
	renames tags across all posts
	*/
	function rename_tag($old_tag,$new_tag)
	{
		// build path and query string
		$queryString =
		'/api/tags/rename?' // action
		.'&old='.urlencode($old_tag)
		.'&new='.urlencode($new_tag);
		
		// send
		return $this->send($queryString);
	}
	
	/*
	function: http://del.icio.us/api/inbox/get?
			&dt= filter by this date
	returns a list of inbox entries
	*/
	function get_inbox($dt)
	{
		// build path and query string
		$queryString =
		'/api/inbox/get?' // action
		.'&dt='.urlencode($dt);
		
		// send
		return $this->send($queryString);
	}
	
	/*
	function: http://del.icio.us/api/inbox/dates?
	returns a list of dates containing inbox entries
	*/
	function get_inbox_dates()
	{
		// build path and query string
		$queryString =
		'/api/inbox/dates?'; // action
		
		// send
		return $this->send($queryString);
	}
	
	/*
	function: http://del.icio.us/api/inbox/subs?
	returns a list of your subscriptions
	*/
	function get_subscriptions()
	{
		// build path and query string
		$queryString =
		'/api/inbox/subs?'; // action
		
		// send
		return $this->send($queryString);
	}
	
	/*
	function: http://del.icio.us/api/inbox/sub?
		&user= username
		&tag = tag - optional, leave blank for all posts
	adds a subscription
	*/
	function subscribe($user,$tag=false)
	{
		// build path and query string
		$queryString =
		'/api/inbox/sub?' // action
		.'&user='.urlencode($user);
		
		if($tag)
		{
			$queryString .= '&tag='.urlencode($tag);
		}
		
		// send
		return $this->send($queryString);
	}
	
	/*
	function: http://del.icio.us/api/inbox/unsub?
		&user= username
		&tag = tag - optional, leave blank for all posts
	removes a subscription
	*/
	function unsubscribe($user,$tag=false)
	{
		// build path and query string
		$queryString =
		'/api/inbox/unsub?' // action
		.'&user='.urlencode($user);
		
		if($tag)
		{
			$queryString .= '&tag='.urlencode($tag);
		}
		
		// send
		return $this->send($queryString);
	}
	
	/*
	function: http://del.icio.us/api/posts/delete?url=
	deletes a post
	*/
	function delete_post($url)
	{
		// build path and query string
		$queryString =
		'/api/posts/delete'
		.'?url='.urlencode($url);
		
		// send
		return $this->send($queryString);
	}
} 


?>