<?php
/*
 **************************************************************************
 *
 * OpenSRS-PHP
 *
 * Copyright (C) 2000, 2001, 2002, 2003 Colin Viebrock 
 *   and easyDNS Technologies Inc.
 *
 **************************************************************************
 *
 * 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
 *
 **************************************************************************
 *
 * vim: set expandtab tabstop=4 shiftwidth=4:
 * $Id: OPS.php,v 1.1 2004/09/30 09:25:23 Tony Exp $
 *
 **************************************************************************
 */


require_once 'PEAR.php';

class OPS extends PEAR {

	var $_OPS_VERSION	= '0.9';
	var $_OPT			= '';
	var $_SPACER		= ' ';		/* indent character */
	var $_CRLF			= "\n";
	var $_MSGTYPE_STD	= 'standard';
	var $_SESSID;
	var $_MSGCNT;

	var $CRLF			= "\r\n";

	var $_log			= array();

	var $_data;
	var $_pointers;
	var $_last_was_data_block;



	/**
	 * Class constructor
	 *
	 * Initialize variables, logs, etc.
	 *
	 * @param	array	allows for setting various options (right now, just whether
	 *					to use compression or not on the generated XML)
	 */

	function OPS($args=false)
	{

		$this->PEAR();

		if (is_array($args)) {
			if ($args['option']=='compress') {
				$this->_OPT	= 'compress';
				$this->_SPACER	= '';
				$this->_CRLF	= '';
			}
		}

		$this->_SESSID = getmypid();
		$this->_MSGCNT = 0;

		$this->_log('raw','i','OPS Raw Log:');
		$this->_log('raw','i','Initialized '.date('r') );

		$this->_log('xml','i','OPS XML Log:');
		$this->_log('xml','i','Initialized '.date('r') );

	}




	/**
	 * Writes a message to a socket (buffered IO)
	 *
	 * @param	int 	socket handle
	 *
	 * @param	string 	message to write
	 *
	 */

	function writeData(&$fh,$msg)
	{
		$len = strlen($msg);
		fputs($fh, 'Content-Length: ' . $len . $this->CRLF . $this->CRLF);
		fputs($fh, $msg, $len );

		$this->_log('raw', 'w', $msg, $len);
	}


	/**
	 * Encodes and writes a message to a socket
	 *
	 * @param	int 	socket handle
	 *
	 * @param	string 	message to encode and write
	 *
	 */

	function writeMessage(&$fh, $hr )
	{
		$msg = $this->encode( $hr );
		$this->writeData($fh, $msg );
	}


	/**
	 * Reads data from a socket
	 *
	 * @param	int 	socket handle
	 *
	 * @param	int 	timeout for read
	 *
	 * @return	mixed	buffer with data, or an error for a short read
	 *
	 */

	function readData(&$fh, $timeout=5)
	{
		$len = 0;

		/* PHP doesn't have timeout for fread ... we just set the timeout for the socket */

		socket_set_timeout($fh, $timeout);

		$line = fgets($fh, 4000);

		if ($this->socketStatus($fh)) {
			return false;
		}

		if (!$len && preg_match('/^\s*Content-Length:\s+(\d+)\s*\r\n/i', $line, $matches ) ) {
			$len = (int)$matches[1];
		} else {
			$this->_log('raw', 'e', 'UNEXPECTED READ: No Content-Length' );
			$this->_log('raw', 'r', $line);
			return false;
		}

		/* read the empty line */

		$line = fread($fh, 2);
		if ($this->socketStatus($fh)) {
			return false;
		}

		if ($line!=$this->CRLF) {
			$this->_log('raw', 'e', 'UNEXPECTED READ: No CRLF');
			$this->_log('raw', 'r', $line);
			return false;
		}

		$line = '';
		while (strlen($line) < $len) {
			$line .= fread($fh, $len);
			if ($this->socketStatus($fh)) {
				return false;
			}
		}

		if ($line) {
			$buf = $line;
			$this->_log('raw', 'r', $line);
		} else {
			$buf = false;
			$this->_log('raw', 'e', 'NEXT LINE SHORT READ (should be '.$len.')' );
			$this->_log('raw', 'r', $line);
		}

		return $buf;
	}


	/**
	 * Reads and decodes data from a socket
	 *
	 * @param	int 	socket handle
	 *
	 * @param	int 	timeout for read
	 *
	 * @return	mixed	associative array of data, or an error
	 *
	 */
	function readMessage(&$fh, $timeout=5)
	{
		$buf = $this->readData($fh, $timeout);
		return ( $buf ? $this->decode($buf) : false );
	}



	/**
	 * Checks a socket for timeout or EOF
	 *
	 * @param	int 		socket handle
	 *
	 * @return	boolean 	true if the socket has timed out or is EOF
	 *
	 */

	function socketStatus(&$fh)
	{
		$return = false;
		if (is_resource($fh)) {
			$temp = socket_get_status($fh);
			if ($temp['timed_out']) {
				$this->_log('raw', 'e', 'SOCKET TIMED OUT');
				$return = true;
			}
			if ($temp['eof']) {
				$this->_log('raw', 'e', 'SOCKET EOF');
				$return = true;
			}
			unset($temp);
		}
		return $return;
	}



	/**
	 * Internal method to generate error codes hashes
	 *
	 * @param	int 		error code
	 *
	 * @param	string		error message
	 *
	 * @return	array 		error hash
	 *
	 */

	function _opsError($err_code,$err_text)
	{
		return array(
			'response_code'	=> $err_code,
			'response_text'	=> $err_text,
			'is_success'	=> 0
		);
	}



#
#	DECODING METHODS
#	Converts XML OPS messages into PHP data
#


	/**
	 * Accepts an OPS protocol message or an file handle
	 * and decodes the data into a PHP array
	 *
	 * @param	string 		OPS message
	 *
	 * @return	mixed		PHP array, or error
	 *
	 */

	function decode($in)
	{

		$ops_msg = '';

		/* determine if we were passed a string or file handle */

		if (is_resource($in)) {
			# read the file into a string, then process as usual
				while (!feof($in)) {
					$ops_msg .= fgets($in, 400);
				}
		} else {
			$ops_msg = $in;
		}


		/* log it first */

		$this->_log('xml', 'r', $ops_msg);


		/* decode and return */

		return $this->XML2PHP($ops_msg);


	}


	/**
	 * XML Parser that converts an OPS protocol message into a PHP array
	 *
	 * @param	string 		OPS message
	 *
	 * @return	mixed		PHP array, or error
	 *
	 */

	function XML2PHP($msg) {

		$this->_data = NULL;

		$xp = xml_parser_create();
		xml_parser_set_option($xp, XML_OPTION_CASE_FOLDING, false);
		xml_parser_set_option($xp, XML_OPTION_SKIP_WHITE, true);
		xml_parser_set_option($xp, XML_OPTION_TARGET_ENCODING, 'ISO-8859-1');

		if (!xml_parse_into_struct($xp,$msg,$vals,$index)) {
			$error = sprintf('XML error: %s at line %d',
				xml_error_string(xml_get_error_code($xp)),
				xml_get_current_line_number($xp)
			);
			xml_parser_free($xp);
			return $this->raiseError($error);
		}

		xml_parser_free($xp);

		$temp = $depth = array();

		foreach($vals as $value) {

			switch ($value['tag']) {
			  case 'OPS_envelope':
			  case 'header':
			  case 'body':
			  case 'data_block':
				break;

			  case 'version':
			  case 'msg_id':
			  case 'msg_type':
				$key = '_OPS_' . $value['tag'];
				$temp[$key] = $value['value'];
				break;

			  case 'item':
				$key = $value['attributes']['key'];

				switch ($value['type']) {
				  case 'open':
					array_push($depth, $key);
					break;

				  case 'complete':
					array_push($depth, $key);
					$p = join('::',$depth);
					$temp[$p] = $value['value'];
					array_pop($depth);
					break;

				  case 'close':
					array_pop($depth);
					break;

				}

				break;

			  case 'dt_assoc':
			  case 'dt_array':
				break;

			}
		}

		foreach ($temp as $key=>$value) {

			$levels = explode('::',$key);
			$num_levels = count($levels);

			if ($num_levels==1) {
				$this->_data[$levels[0]] = $value;
			} else {
				$pointer = &$this->_data;
				for ($i=0; $i<$num_levels; $i++) {
					if ( !isset( $pointer[$levels[$i]] ) ) {
						$pointer[$levels[$i]] = array();
					}
					$pointer = &$pointer[$levels[$i]];
				}
				$pointer = $value;
			}

		}

		return ($this->_data);

	}



#
#	ENCODING METHODS
#	Converts PHP data into XML OPS messages
#


	/**
	 * Converts a PHP array into an OPS message
	 *
	 * @param	array		PHP array
	 *
	 * @return 	string		OPS XML message
	 *
	 */

	function encode($array)
	{

		$this->_MSGCNT++;
		$msg_id = $this->_SESSID + $this->_MSGCNT;			/* addition removes the leading zero */
		$msg_type = $this->_MSGTYPE_STD;


		if ($array['protocol']) {
			$array['protocol'] = strtoupper($array['protocol']);
		}
		if ($array['action']) {
			$array['action'] = strtoupper($array['action']);
		}
		if ($array['object']) {
			$array['object'] = strtoupper($array['object']);
		}

		$xml_data_block = $this->PHP2XML($array);

		$ops_msg = '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>' . $this->_CRLF .
			'<!DOCTYPE OPS_envelope SYSTEM "ops.dtd">' . $this->_CRLF .
			'<OPS_envelope>' . $this->_CRLF .
			$this->_SPACER . '<header>' . $this->_CRLF .
			$this->_SPACER . $this->_SPACER . '<version>' . $this->_OPS_VERSION . '</version>' . $this->_CRLF .
			$this->_SPACER . $this->_SPACER . '<msg_id>' . $msg_id . '</msg_id>' . $this->_CRLF .
			$this->_SPACER . $this->_SPACER . '<msg_type>' . $msg_type . '</msg_type>' . $this->_CRLF .
			$this->_SPACER . '</header>' . $this->_CRLF .
			$this->_SPACER . '<body>' . $this->_CRLF .
			$xml_data_block . $this->_CRLF .
			$this->_SPACER . '</body>' . $this->_CRLF .
			'</OPS_envelope>';

		# log it

		$this->_log('xml', 'w', $ops_msg);

		return $ops_msg;

	}



	/**
	 * Converts a PHP array into an OPS data_block tag
	 *
	 * @param	array		PHP array
	 *
	 * @return 	string		OPS data_block tag
	 *
	 */

	function PHP2XML($data)
	{
		return str_repeat($this->_SPACER,2) . '<data_block>' .
			$this->_convertData($data, 3) .
			$this->_CRLF . str_repeat($this->_SPACER,2) . '</data_block>';
	}


	/**
	 * Recursivly converts PHP data into XML
	 *
	 * @param	mixed		PHP array or data
	 *
	 * @param	int			ident level
	 *
	 * @return 	string		XML string
	 *
	 */

	function _convertData(&$array, $indent=0) 
	{
		$string = '';
		$IND = str_repeat($this->_SPACER,$indent);

		if (is_array($array)) {

			if ($this->_is_assoc($array)) {		# HASH REFERENCE
				$string .= $this->_CRLF . $IND . '<dt_assoc>';
				$end = '</dt_assoc>';
			} else {				# ARRAY REFERENCE
				$string .= $this->_CRLF . $IND . '<dt_array>';
				$end = '</dt_array>';
			}

			foreach ($array as $k=>$v) {

				$indent++;

				/* don't encode some types of stuff */
				if ((gettype($v)=='resource') || (gettype($v)=='user function') || (gettype($v)=='unknown type')) {
					continue;
				}

				$string .= $this->_CRLF . $IND . '<item key="' . $k . '"';

				if (gettype($v)=='object' && get_class($v)) {
					$string .= ' class="' . get_class($v) . '"';
				}

				$string .= '>';

				if (is_array($v) || is_object($v)) {
					$string .= $this->_convertData($v, $indent+1);
					$string .= $this->_CRLF . $IND . '</item>';
				} else {
					$string .= $this->_quoteXMLChars($v) . '</item>';
				}

				$indent--;
			}

			$string .= $this->_CRLF . $IND . $end;

		} else {					# SCALAR

			$string .= $this->_CRLF . $IND . '<dt_scalar>' .
				$this->_quoteXMLChars($array) . '</dt_scalar>';
		}

		return $string;

	}



	/**
	 * Quotes special XML characters
	 *
	 * @param	string		string to quote
	 *
	 * @return 	string		quoted string
	 *
	 */

	function _quoteXMLChars($string) 
	{
		$search  = array ('&', '<', '>', "'", '"');
		$replace = array ('&amp;', '&lt;', '&gt;', '&apos;', '&quot;');
		$string = str_replace($search, $replace, $string);
		$string = utf8_encode($string);
		return $string;
	}




	/**
	 * Determines if an array is associative or not, since PHP
	 * doesn't really distinguish between the two, but Perl/OPS does
	 *
	 * @param	array		array to check
	 *
	 * @return 	boolean		true if the array is associative
	 *
	 */

	function _is_assoc(&$array)
	{
	        if (is_array($array)) {
	                foreach ($array as $k=>$v) {
	                        if (!is_int($k)) {
	                                return true;
	                        }
	                }
	        }
	        return false;
	}




	/**
	 * Internal loggging method
	 *
	 * @param	string 		which log to log to
	 *
	 * @param	string		type of log message ('r'ead, 'w'rite, 'i'nfo or 'e'rror)
	 *
	 * @param	int			message
	 *
	 */

	function _log($log, $type, $msg)
	{
		$types = array(
			'r'	=> 'read',
			'w'	=> 'write',
			'e'	=> 'error',
			'i' => 'info'
		);

		if ($log=='xml') {
			$this->log[$log][] = sprintf("[% 6s:%06d] %s\n",
				strtoupper($types[$type]),
				($type=='e' || $type=='i') ? 0 : strlen($msg),
				$msg
			);
		} else {
			$this->log[$log][] = sprintf("[% 6s:%06d] %s\n",
				strtoupper($types[$type]),
				($type=='e' || $type=='i') ? 0 : strlen($msg),
				($type=='e' || $type=='i') ? $msg : bin2hex($msg)
			);
		}
	}


	/**
	 * Show internal log
	 *
	 * @param	string 		which log to log show, 'raw' or 'xml'
	 *
	 * @param	string		format to display: 'html' (default) or 'raw'
	 *
	 */

	function showLog($log, $format='html')
	{
		echo '<PRE>';
		foreach ($this->log[$log] as $line) {
			switch ($format) {
			  case 'raw':
				echo $line . "\n";
				break;
			  case 'html':
			  default:
				echo htmlEntities($line) . "\n";
				break;
			}
		}
		echo '</PRE>';
	}



}