<?php

/**
 * Handlers for each of the QBWC SOAP server required methods
 *
 * Copyright (c) 2010 Keith Palmer / ConsoliBYTE, LLC.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.opensource.org/licenses/eclipse-1.0.php
 *
 * The QuickBooks Web Connector requires that your SOAP server be able to
 * handle six basic methods. Each of the six methods are implemented in this
 * class and called by the QuickBooks_Server class instance.
 *
 * These methods in turn will call the action handlers you register with the
 * SOAP server, and also log quite a bit of debugging information to the
 * database so that you can see what's happening during the QBWC exchange with
 * your SOAP server.
 *
 * @author Keith Palmer <keith@consolibyte.com>
 * @license LICENSE.txt
 *
 * @package QuickBooks
 * @subpackage Server
 */

/**
 * Various QuickBooks related utilities methods
 */
QuickBooks_Loader::load('/QuickBooks/Utilities.php');

/**
 * Functions for calling callbacks (functions, static methods, object methods, etc.)
 */
QuickBooks_Loader::load('/QuickBooks/Callbacks.php');

/**
 * Response container for calls to ->authenticate()
 */
QuickBooks_Loader::load('/QuickBooks/WebConnector/Result/Authenticate.php');

/**
 * Response container for calls to ->closeConnection()
 */
QuickBooks_Loader::load('/QuickBooks/WebConnector/Result/CloseConnection.php');

/**
 * Response container for calls to ->connectionError()
 */
QuickBooks_Loader::load('/QuickBooks/WebConnector/Result/ConnectionError.php');

/**
 * Response container for calls to ->getLastError()
 */
QuickBooks_Loader::load('/QuickBooks/WebConnector/Result/GetLastError.php');

/**
 * Response container for calls to ->receiveResponseXML()
 */
QuickBooks_Loader::load('/QuickBooks/WebConnector/Result/ReceiveResponseXML.php');

/**
 * Response container for calls to ->sendRequestXML()
 */
QuickBooks_Loader::load('/QuickBooks/WebConnector/Result/SendRequestXML.php');

/**
 * Response container for calls to ->getServerVersion()
 */
QuickBooks_Loader::load('/QuickBooks/WebConnector/Result/ServerVersion.php');

/**
 * Response container for calls to ->clientVersion()
 */
QuickBooks_Loader::load('/QuickBooks/WebConnector/Result/ClientVersion.php');

/**
 * Hook which gets called when the ->authenticate() method gets called
 * @param string
 */
define('QUICKBOOKS_HANDLERS_HOOK_AUTHENTICATE', 'QuickBooks_Handlers::authenticate');

/**
 * Hook which gets called when the ->clientVersion() method gets called
 * @var string
 */
define('QUICKBOOKS_HANDLERS_HOOK_CLIENTVERSION', 'QuickBooks_Handlers::clientVersion');

/**
 * Hook which gets called when the ->closeConnection() method gets called
 * @var string
 */
define('QUICKBOOKS_HANDLERS_HOOK_CLOSECONNECTION', 'QuickBooks_Handlers::closeConnection');

/**
 * Hook which gets called when the ->connectionError() method gets called
 * @var string
 */
define('QUICKBOOKS_HANDLERS_HOOK_CONNECTIONERROR', 'QuickBooks_Handlers::connectionError');

/**
 *
 */
define('QUICKBOOKS_HANDLERS_HOOK_GETINTERACTIVEURL', 'QuickBooks_Handlers::getInteractiveURL');

/**
 *
 */
define('QUICKBOOKS_HANDLERS_HOOK_GETLASTERROR', 'QuickBooks_Handlers::getLastError');

/**
 *
 *
 */
define('QUICKBOOKS_HANDLERS_HOOK_INTERACTIVEDONE', 'QuickBooks_Handlers::interactiveDone');

/**
 *
 *
 */
define('QUICKBOOKS_HANDLERS_HOOK_INTERACTIVEREJECTED', 'QuickBooks_Handlers::interactiveRejected');

/**
 *
 *
 */
define('QUICKBOOKS_HANDLERS_HOOK_RECEIVERESPONSEXML', 'QuickBooks_HandlersS::receiveResponseXML');

/**
 *
 */
define('QUICKBOOKS_HANDLERS_HOOK_SENDREQUESTXML', 'QuickBooks_Handlers::sendRequestXML');

/**
 *
 */
define('QUICKBOOKS_HANDLERS_HOOK_SERVERVERSION', 'QuickBooks_Handlers::serverVersion');

/**
 *
 */
define('QUICKBOOKS_HANDLERS_HOOK_LOGINSUCCESS', 'QuickBooks_Handlers::login-success');

/**
 * Hook which is called when a login fails
 * @var string
 */
define('QUICKBOOKS_HANDLERS_HOOK_LOGINFAILURE', 'QuickBooks_Handlers::login-fail');

/**
 * Alias of {@link QUICKBOOKS_HANDLERS_HOOK_LOGINFAILURE}
 * @var string
 */
define('QUICKBOOKS_HANDLERS_HOOK_LOGINFAIL', QUICKBOOKS_HANDLERS_HOOK_LOGINFAILURE);

/**
 * Hook which is called when recurring events are registered
 * @var string
 */
define('QUICKBOOKS_HANDLERS_HOOK_RECURRING', 'QuickBooks_Handlers::recurring');

/**
 * Hook which is called to report a percentage don
 * @var string
 */
define('QUICKBOOKS_HANDLERS_HOOK_PERCENT', 'QuickBooks_Handlers::percent');

/**
 * Handlers for each of the QBWC SOAP server required methods
 */
class QuickBooks_WebConnector_Handlers
{
	const HOOK_AUTHENTICATE = QUICKBOOKS_HANDLERS_HOOK_AUTHENTICATE;
	const HOOK_LOGINSUCCESS = QUICKBOOKS_HANDLERS_HOOK_LOGINSUCCESS;

	/**
	 * Driver object instance for backend of SOAP server
	 * @var QuickBooks_Driver
	 */
	protected $_driver;

	/**
	 * Raw XML input
	 * @var string
	 */
	protected $_input;

	/**
	 * Map of queued actions to function handlers
	 * @var array
	 */
	protected $_map;

	/**
	 *
	 *
	 */
	protected $_instance_map;

	/**
	 * Map of error codes to function handler
	 * @var array
	 */
	protected $_onerror;

	/**
	 *
	 *
	 */
	protected $_instance_onerror;

	/**
	 * Map of hook names to function handlers
	 * @var array
	 */
	protected $_hooks;

	/**
	 *
	 *
	 */
	protected $_instance_hooks;

	/**
	 * Configuration parameters
	 * @var array
	 */
	protected $_config;

	/**
	 * Callback configuration parameters
	 * @var array
	 */
	protected $_callback_config;

	/**
	 * Create the server handler instance
	 *
	 * Optional configuration items should be passed as an associative array with any of these keys:
	 * 	- qb_company_file				The full filesystem path to a specific QuickBooks company file (by default, it will use the currently open company file)
	 * 	- qbwc_min_version				Minimum version of the Web Connector that must be used to connect (by default, any version may connect)
	 * 	- qbwc_wait_before_next_update 	Tell the Web Connector to wait this number of seconds before doign another update
	 * 	- qbwc_min_run_every_n_seconds	Tell the Web Connector to run every n seconds (overrides whatever was in the .QWC web connector configuration file)
	 * 	- qbwc_interactive_url			The URL to use for Interactive QuickBooks Web Connector sessions
	 * 	- server_version				Server version string
	 * 	- authenticate_handler			If you want to use some custom authentication method, put the function name of your custom authentication function here
	 * 	- autoadd_missing_requestid		This defaults to TRUE, if TRUE and you forget to embed a requestID="..." attribute, it will try to automatically add that attribute for you
	 *
	 * @param mixed $dsn_or_conn		DSN connection string for QuickBooks queue
	 * @param array $map				A map of QuickBooks API calls to callback functions/methods
	 * @param array $onerror			A map of QuickBooks error codes to callback functions/methods
	 * @param array $hooks				A map of hook names to callback functions/methods
	 * @param string $input				Raw XML input from QuickBooks API call
	 * @param array $handler_config		An array of configuration options
	 * @param array $driver_config		An array of driver configuration options
	 */
	public function __construct($dsn_or_conn, $map, $onerror, $hooks, $log_level, $input, $handler_config = array(), $driver_config = array(), $callback_config = array())
	{
		$this->_driver = QuickBooks_Utilities::driverFactory($dsn_or_conn, $driver_config, $hooks, $log_level);
		$this->_input = $input;
		$this->_map = $map;
		$this->_onerror = $onerror;

		$this->_hooks = array();
		foreach ($hooks as $hook => $funcs)
		{
			if (!is_array($funcs))
			{
				$funcs = array( $funcs );
			}

			$this->_hooks[$hook] = $funcs;
		}

		$this->_config = $this->_defaults($handler_config);

		$this->_callback_config = $callback_config;

		//$this->_driver->log('Handler is starting up...: ' . print_r($this->_config, true), '', QUICKBOOKS_LOG_DEBUG);
		$this->_log('Handler is starting up...: ' . print_r($this->_config, true), '', QUICKBOOKS_LOG_DEBUG);
	}

	/**
	 * Massage any optional configuration flags
	 *
	 * @param array $config
	 * @return array
	 */
	protected function _defaults($config)
	{
		$url = '?';
		if (isset($_SERVER['REQUEST_URI']))
		{
			$url = $_SERVER['REQUEST_URI'];
		}

		$defaults = array(
			'qb_company_file' => null,					// To force a specific company file to be used
			'qbwc_min_version' => null, 				// Minimum version of the QBWC that must be used to connect
			'qbwc_wait_before_next_update' => null, 	// Tell the QBWC to wait this number of seconds before doing another update
			'qbwc_min_run_every_n_seconds' => null,		// Tell the QBWC to run every n seconds (overrides whatever was in the .QWC web connector configuration file)
			'qbwc_version_warning_message' => null,		// Not implemented...
			'qbwc_version_error_message' => null,  		// Not implemented...
			'qbwc_interactive_url' => null, 			// Provide the URL for an interactive session to the QuickBooks Web Connector
			'autoadd_missing_requestid' => true,
			'check_valid_requestid' => true,
			'server_version' => 'PHP QuickBooks SOAP Server v' . QUICKBOOKS_PACKAGE_VERSION . ' at ' . $url,	// Server version string
			'authenticate' => null, 					// If you want to use some custom authentication scheme (and not the quickbooks_user MySQL table) you can specify your own function here
			'authenticate_dsn' => null, 				//		(backward compat. for 'authenticate')
			'map_application_identifiers' => true, 		// Try to map web application IDs to QuickBooks ListIDs/TxnIDs
			'allow_remote_addr' => array(),
			'deny_remote_addr' => array(),
			'convert_unix_newlines' => true,

			'deny_concurrent_logins' => true,
			'deny_concurrent_timeout' => 60,

			'deny_reallyfast_logins' => true,
			'deny_reallyfast_timeout' => 600,

			'masking' => true,
			);

		$config = array_merge($defaults, $config);

		// Make sure this is an *array* of addresses to allow
		if (!is_array($config['allow_remote_addr']))
		{
			$config['allow_remote_addr'] = array( $config['allow_remote_addr'] );
		}

		// Make sure this is an *array* of addresses to deny
		if (!is_array($config['deny_remote_addr']))
		{
			$config['deny_remote_addr'] = array( $config['deny_remote_addr'] );
		}

		$config['autoadd_missing_requestid'] = (boolean) $config['autoadd_missing_requestid'];
		$config['check_valid_requestid'] = (boolean) $config['check_valid_requestid'];
		$config['map_application_identifiers'] = (boolean) $config['map_application_identifiers'];
		$config['convert_unix_newlines'] = (boolean) $config['convert_unix_newlines'];

		$config['deny_concurrent_logins'] = (boolean) $config['deny_concurrent_logins'];
		$config['deny_concurrent_timeout'] = (int) max(1, $config['deny_concurrent_timeout']);

		$config['deny_reallyfast_logins'] = (boolean) $config['deny_reallyfast_logins'];
		$config['deny_reallyfast_timeout'] = (int) max(1, $config['deny_reallyfast_timeout']);

		return $config;
	}

	/**
	 * Check if a given remote address (IP address) is allowed based on allow and deny arrays
	 *
	 * @param string $remoteaddr
	 * @param array $allow
	 * @param array $deny
	 * @return boolean
	 */
	protected function _checkRemote($remoteaddr, $arr_allow, $arr_deny)
	{
		return QuickBooks_Utilities::checkRemoteAddress($remoteaddr, $arr_allow, $arr_deny);
	}

	/**
	 * Log a message to the error/debug log
	 *
	 * @param string $msg
	 * @param string $ticket
	 * @param integer $level
	 * @return boolean
	 */
	protected function _log($msg, $ticket, $level = QUICKBOOKS_LOG_NORMAL)
	{
		$Driver = $this->_driver;

		if ($this->_config['masking'])
		{
			$msg = QuickBooks_Utilities::mask($msg);
		}

		if ($Driver)
		{
			return $Driver->log($msg, $ticket, $level);
		}

		return false;
	}

	/**
	 * Queue up recurring events that are overdue to be run
	 *
	 * @param string $ticket
	 * @return boolean
	 */
	protected function _handleRecurringEvents($ticket)
	{
		if ($user = $this->_driver->authResolve($ticket))
		{
			while ($next = $this->_driver->recurDequeue($user, true))
			{
				//$this->_driver->log('Dequeued a recurring event, enqueuing!', $ticket, QUICKBOOKS_LOG_VERBOSE);
				$this->_log('Dequeued a recurring event, enqueuing!', $ticket, QUICKBOOKS_LOG_VERBOSE);

				$extra = null;
				if ($next['extra'])
				{
					$extra = unserialize($next['extra']);
				}

				//print_r($next);

				$hookerr = '';
				$this->_callHook($ticket,
					QUICKBOOKS_HANDLERS_HOOK_RECURRING,
					null, 					//$this->_constructRequestID($next['qb_action'], $next['ident']),
					$next['qb_action'],
					$next['ident'],
					$extra,
					$hookerr);
				// $ticket, $hook, $requestID, $action, $ident, $extra, &$err, $xml = '', $qb_identifiers = array()

				//print_r($next);
				//exit;

				// (boolean) $next['replace']
				// 							$user, $action, $ident, $replace = true, $priority = 0, $extra = null, $qbxml = null
				$this->_driver->queueEnqueue($user, $next['qb_action'], $next['ident'], true, (int) $next['priority'], $extra, $next['qbxml']);
			}

			return true;
		}

		return false;
	}

	/**
	 * Authenticate method for the QuickBooks Web Connector SOAP service
	 *
	 * The authenticate method is called when the Web Connector establishes a
	 * connection with the SOAP server in order to ensure that there is work to
	 * do and that the Web Connector is allowed to connect/that it actually is
	 * the Web Connector that is connecting and sending us messages.
	 *
	 * The stdClass object that is received as a parameter will have two
	 * members:
	 * 	- strUserName	The username provided in the QWC file to the Web Connector
	 * 	- strPassword 	The password the end-user enters into the QuickBooks Web Connector application
	 *
	 * The return object should be an array with two elements. The first
	 * element is a generated login ticket (or an empty string if the login
	 * failed) and the second string is either "none" (for successful log-ins
	 * with nothing to do in the queue) or "nvu" if the login failed.
	 *
	 * The following user-defined hooks are invoked:
	 * 	- QUICKBOOKS_HANDLERS_HOOK_AUTHENTICATE
	 * 	- QUICKBOOKS_HANDLERS_HOOK_LOGINSUCCESS
	 * 	- QUICKBOOKS_HANDLERS_HOOK_LOGINFAILURE
	 *
	 * @param stdClass $obj						The SOAP object that gets sent by the Web Connector
	 * @return QuickBooks_Result_Authenticate	A container object to send back to the Web Connector
	 */
	public function authenticate($obj)
	{
		//$this->_driver->log('authenticate()', '', QUICKBOOKS_LOG_VERBOSE);
		$this->_log('authenticate()', '', QUICKBOOKS_LOG_VERBOSE);

		$ticket = '';
		$status = '';

		// Authenticate login hook
		$hookdata = array(
			'username' => $obj->strUserName,
			'password' => $obj->strPassword,
			);
		$hookerr = '';
		$this->_callHook($ticket, QUICKBOOKS_HANDLERS_HOOK_AUTHENTICATE, null, null, null, null, $hookerr, null, array(), $hookdata);

		// Remote address allow/deny
		if (false == $this->_checkRemote($_SERVER['REMOTE_ADDR'], $this->_config['allow_remote_addr'], $this->_config['deny_remote_addr']))
		{
			//$this->_driver->log('Connection from remote address rejected: ' . $_SERVER['REMOTE_ADDR'], null, QUICKBOOKS_LOG_VERBOSE);
			$this->_log('Connection from remote address rejected: ' . $_SERVER['REMOTE_ADDR'], null, QUICKBOOKS_LOG_VERBOSE);

			return new QuickBooks_WebConnector_Result_Authenticate('', 'nvu', null, null);
		}

		// If we do either concurrent login checks, or rate-limiting, we need to grab the date/time
		//	of the last connection.
		$authLast = null;
		if ($this->_config['deny_concurrent_logins'] or $this->_config['deny_reallyfast_logins'])
		{
			$authlast = $this->_driver->authLast($obj->strUserName);
		}

		// Check for concurrent logins
		if ($this->_config['deny_concurrent_logins'])
		{
			if ($authlast and
				time() - strtotime($authlast[1]) < $this->_config['deny_concurrent_timeout'])
			{
				$this->_log('Denied concurrent login from: ' . $obj->strUserName, null, QUICKBOOKS_LOG_VERBOSE);

				return new QuickBooks_WebConnector_Result_Authenticate('CONC1234', 'none', null, null);
			}
		}

		// Rate-limiting
		if ($this->_config['deny_reallyfast_logins'])
		{
			if ($authlast and
				time() - strtotime($authlast[1]) < $this->_config['deny_reallyfast_timeout'])
			{
				$this->_log('Denied really fast login from: ' . $obj->strUserName . ' (last login: ' . $authlast[1] . ')', null, QUICKBOOKS_LOG_VERBOSE);

				return new QuickBooks_WebConnector_Result_Authenticate('FAST1234', 'none', null, null);
			}
		}

		// Custom authentication backends
		$override_dsn = $this->_config['authenticate'];

		if (!empty($this->_config['authenticate_dsn']))
		{
			// Backwards compat.
			$override_dsn = $this->_config['authenticate_dsn'];
		}

		$auth = null;

		/*
		if (strlen($override_dsn))
		{
			$override_dsn = str_replace('function://', '', $override_dsn);
		}
		*/

		$company_file = null;
		$wait_before_next_update = null;
		$min_run_every_n_seconds = null;

		$customauth_company_file = null;
		$customauth_wait_before_next_update = null;
		$customauth_min_run_every_n_seconds = null;

		if (is_array($override_dsn) or strlen($override_dsn)) 	// Custom autj
		{
			//if ($auth->authenticate($obj->strUserName, $obj->strPassword, $customauth_company_file, $customauth_wait_before_next_update, $customauth_min_run_every_n_seconds) and

			//if ($override_dsn($obj->strUserName, $obj->strPassword, $customauth_company_file, $customauth_wait_before_next_update, $customauth_min_run_every_n_seconds) and

			if (QuickBooks_Callbacks::callAuthenticate($this->_driver, $override_dsn, $obj->strUserName, $obj->strPassword, $customauth_company_file, $customauth_wait_before_next_update, $customauth_min_run_every_n_seconds) and
				$ticket = $this->_driver->authLogin($obj->strUserName, $obj->strPassword, $company_file, $wait_before_next_update, $min_run_every_n_seconds, true))
			{
				//$this->_driver->log('Login (' . $parse['scheme'] . '): ' . $obj->strUserName, $ticket, QUICKBOOKS_LOG_DEBUG);
				$this->_log('Login via ' . print_r($override_dsn, true) . ': ' . $obj->strUserName, $ticket, QUICKBOOKS_LOG_DEBUG);

				if ($customauth_company_file)
				{
					$status = $customauth_company_file;
				}
				else if ($company_file)
				{
					$status = $company_file;
				}
				else if ($this->_config['qb_company_file'])
				{
					$status = $this->_config['qb_company_file'];
				}

				if ((int) $customauth_wait_before_next_update)
				{
					$wait_before_next_update = (int) $customauth_wait_before_next_update;
				}
				else if ((int) $wait_before_next_update)
				{
					;
				}
				else if ((int) $this->_config['qbwc_wait_before_next_update'])
				{
					$wait_before_next_update = (int) $this->_config['qbwc_wait_before_next_update'];
				}

				if ((int) $customauth_min_run_every_n_seconds)
				{
					$min_run_every_n_seconds = (int) $customauth_min_run_every_n_seconds;
				}
				else if ((int) $min_run_every_n_seconds)
				{
					;
				}
				else if ((int) $this->_config['qbwc_min_run_every_n_seconds'])
				{
					$min_run_every_n_seconds = (int) $this->_config['qbwc_min_run_every_n_seconds'];
				}

				// Call login hook
				$hookdata = array(
					'authenticate_dsn' => $override_dsn,
					'username' => $obj->strUserName,
					'password' => $obj->strPassword,
					'ticket' => $ticket,
					'qb_company_file' => $status,
					'qbwc_wait_before_next_update' => $wait_before_next_update,
					'qbwc_min_run_every_n_seconds' => $min_run_every_n_seconds,
					);
				$hookerr = '';
				$this->_callHook($ticket, QuickBooks_WebConnector_Handlers::HOOK_LOGINSUCCESS, null, null, null, null, $hookerr, null, array(), $hookdata);

				// Move any recurring events that are due to the queue table
				$this->_handleRecurringEvents($ticket);

				if (!$this->_driver->queueDequeue($obj->strUserName))
				{
					$status = 'none';
				}

				// Login success (with a custom login handler)!
			}
			else
			{
				//$this->_driver->log('Login failed (' . $parse['scheme'] . '): ' . $obj->strUserName, '', QUICKBOOKS_LOG_DEBUG);
				$this->_log('Login failed: ' . $obj->strUserName, '', QUICKBOOKS_LOG_DEBUG);

				$hookdata = array(
					'authenticate_dsn' => $override_dsn,
					'username' => $obj->strUserName,
					'password' => $obj->strPassword,
					);
				$hookerr = '';
				$this->_callHook(null, QUICKBOOKS_HANDLERS_HOOK_LOGINFAILURE, null, null, null, null, $hookerr, null, array(), $hookdata);

				$ticket = '';
				$status = 'nvu'; // Invalid username/password
			}

			return new QuickBooks_WebConnector_Result_Authenticate($ticket, $status, $wait_before_next_update, $min_run_every_n_seconds);
		}
		else	// Standard authentication
		{
			if ($ticket = $this->_driver->authLogin($obj->strUserName, $obj->strPassword, $company_file, $wait_before_next_update, $min_run_every_n_seconds))
			{
				//$this->_driver->log('Login: ' . $obj->strUserName, $ticket, QUICKBOOKS_LOG_DEBUG);
				$this->_log('Login: ' . $obj->strUserName, $ticket, QUICKBOOKS_LOG_DEBUG);

				if (!strlen($company_file) and $this->_config['qb_company_file'])
				{
					$status = $this->_config['qb_company_file'];
				}
				else if (strlen($company_file))
				{
					$status = $company_file;
				}

				if (! (int) $wait_before_next_update and (int) $this->_config['qbwc_wait_before_next_update'])
				{
					$wait_before_next_update = (int) $this->_config['qbwc_wait_before_next_update'];
				}

				if (! (int) $min_run_every_n_seconds and (int) $this->_config['qbwc_min_run_every_n_seconds'])
				{
					$min_run_every_n_seconds = (int) $this->_config['qbwc_min_run_every_n_seconds'];
				}

				$hookdata = array(
					'username' => $obj->strUserName,
					'password' => $obj->strPassword,
					'ticket' => $ticket,
					'qb_company_file' => $status,
					'qbwc_wait_before_next_update' => $wait_before_next_update,
					'qbwc_min_run_every_n_seconds' => $min_run_every_n_seconds,
					);
				$hookerr = '';
				$this->_callHook($ticket, QUICKBOOKS_HANDLERS_HOOK_LOGINSUCCESS, null, null, null, null, $hookerr, null, array(), $hookdata);

				$this->_handleRecurringEvents($ticket);

				if (!$this->_driver->queueDequeue($obj->strUserName))
				{
					$status = 'none'; // Good login, but there isn't anything in the queue
				}

				// Login success!
			}
			else
			{
				//$this->_driver->log('Login failed: ' . $obj->strUserName, '', QUICKBOOKS_LOG_DEBUG);
				$this->_log('Login failed: ' . $obj->strUserName, '', QUICKBOOKS_LOG_DEBUG);

				$hookdata = array(
					'username' => $obj->strUserName,
					'password' => $obj->strPassword,
					);
				$hookerr = '';
				$this->_callHook(null, QUICKBOOKS_HANDLERS_HOOK_LOGINFAILURE, null, null, null, null, $hookerr, null, array(), $hookdata);

				$ticket = '';
				$status = 'nvu'; // Invalid username/password
			}

			return new QuickBooks_WebConnector_Result_Authenticate($ticket, $status, $wait_before_next_update, $min_run_every_n_seconds);
		}
	}

	/**
	 * SendRequestXML method for the QuickBooks Web Connector SOAP server - Generate and send a request to QuickBooks
	 *
	 * The QuickBooks Web Connector calls this method to ask for things to do.
	 * So, calling this method is the Web Connectors way of saying: "Please
	 * send me a command so that I can pass that command on to QuickBooks."
	 * After it passes the command to QuickBooks, it will pass the response
	 * back via a call to receiveResponseXML().
	 *
	 * The stdClass object passed as a parameter should contain these members:
	 * 	- ticket				The login session ticket
	 *  - strHCPResponse
	 * 	- strCompanyFileName
	 * 	- qbXMLCountry			The country code for whatever version of QuickBooks is sitting behind the Web Connector
	 * 	- qbXMLMajorVers		The major version code of the QuickBooks web connector
	 * 	- qbXMLMinorVers 		The minor version code of the QuickBooks web connector
	 *
	 * You should return either an empty string "" to signal an error state, or
	 * a valid qbXML or qbposXML request.
	 *
	 * The following user-defined hooks are invoked by this method:
	 * 	- QUICKBOOKS_HANDLERS_HOOK_SENDREQUESTXML
	 *
	 * @param stdClass $obj
	 * @return QuickBooks_Result_SendRequestXML
	 */
	public function sendRequestXML($obj)
	{
		//$this->_driver->log('sendRequestXML()', $obj->ticket, QUICKBOOKS_LOG_VERBOSE);
		$this->_log('sendRequestXML()', $obj->ticket, QUICKBOOKS_LOG_VERBOSE);

		if ($this->_driver->authCheck($obj->ticket))
		{
			$user = $this->_driver->authResolve($obj->ticket);

			$hookdata = array(
				'username' => 			$user,
				'ticket' => 			$obj->ticket,
				'strHCPResponse' => 	$obj->strHCPResponse,
				'strCompanyFileName' => $obj->strCompanyFileName,
				'qbXMLCountry' => 		$obj->qbXMLCountry,
				'qbXMLMajorVers' => 	$obj->qbXMLMajorVers,
				'qbXMLMinorVers' => 	$obj->qbXMLMinorVers,
				);
			$hookerr = '';
			$this->_callHook($obj->ticket, QUICKBOOKS_HANDLERS_HOOK_SENDREQUESTXML, null, null, null, null, $hookerr, null, array(), $hookdata);
			// _callHook($ticket, $hook, $requestID, $action, $ident, $extra, &$err, $xml = '', $qb_identifiers = array(), $hook_data = array())

			// Move recurring events which are due to run to the queue table
			// 	We *CAN'T* re-register recurring events here, otherwise, we run
			//	the risk of re-adding an event which has occured, *before* the
			//	entire session has finishing running. Thus, we'd create an
			//	infinite loop of web connector that would never end.
			//$this->_handleRecurringEvents($obj->ticket);

			if ($next = $this->_driver->queueDequeue($user, true)) // Fetch the next action/command from the queue
			{
				//$this->_driver->log('Dequeued: ( ' . $next['qb_action'] . ', ' . $next['ident'] . ' ) ', $obj->ticket, QUICKBOOKS_LOG_DEBUG);
				$this->_log('Dequeued: ( ' . $next['qb_action'] . ', ' . $next['ident'] . ' ) ', $obj->ticket, QUICKBOOKS_LOG_DEBUG);
				//$this->_driver->queueStatus($obj->ticket, $next['qb_action'], $next['ident'], QUICKBOOKS_STATUS_PROCESSING);
				$this->_driver->queueStatus($obj->ticket, $next['quickbooks_queue_id'], QUICKBOOKS_STATUS_PROCESSING);

				/*
				// Here's a strange case, interactive mode handler
				if ($next['qb_action'] == QUICKBOOKS_INTERACTIVE_MODE)
				{
					// Set the error to "Interactive mode"
					$this->_driver->errorLog($obj->ticket, QUICKBOOKS_ERROR_OK, QUICKBOOKS_INTERACTIVE_MODE);

					// This will cause ->getLastError() to be called, and ->getLastError() will then return the string "Interactive mode" which will cause QuickBooks to call ->getInteractiveURL() and start an interactive session... I think...?
					return new QuickBooks_Result_SendRequestXML('');
				}
				*/

				$extra = '';
				if ($next['extra'])
				{
					$extra = unserialize($next['extra']);
				}

				$err = '';
				$xml = '';

				//$last_action_time = $this->_driver->queueActionLast($user, $next['qb_action']);
				//$last_actionident_time = $this->_driver->queueActionIdentLast($user, $next['qb_action'], $next['ident']);
				$last_action_time = null;
				$last_actionident_time = null;

				// Call the mapped function that should generate an appropriate qbXML request
				$xml = $this->_callMappedFunction(0, $user, $next['quickbooks_queue_id'], $next['qb_action'], $next['ident'], $extra, $err, $last_action_time, $last_actionident_time, $obj->qbXMLMajorVers . '.' . $obj->qbXMLMinorVers, $obj->qbXMLCountry, $next['qbxml']);

				// Make sure there's no whitespace around it
				$xml = trim($xml);

				// NoOp can be returned to skip this current operation. This will cause getLastError
				//	to be called, at which point NoOp should be returned to tell the Web
				//	Connector to then pause for 5 seconds before asking for another request.
				if ($xml == QUICKBOOKS_NOOP)
				{
					$this->_driver->errorLog($obj->ticket, 0, QUICKBOOKS_NOOP);

					// Mark it as a NoOp to remove it from the queue
					//$this->_driver->queueStatus($obj->ticket, $next['qb_action'], $next['ident'], QUICKBOOKS_STATUS_NOOP, 'Handler function returned: ' . QUICKBOOKS_NOOP);
					$this->_driver->queueStatus($obj->ticket, $next['quickbooks_queue_id'], QUICKBOOKS_STATUS_NOOP, 'Handler function returned: ' . QUICKBOOKS_NOOP);

					return new QuickBooks_WebConnector_Result_SendRequestXML('');
				}

				// If the requestID="..." attribute was not specified, we can try to automatically add it to the request
				$requestID = null;
				if (!($requestID = $this->_extractRequestID($xml)) and
					$this->_config['autoadd_missing_requestid'])
				{
					// Find the <DoSomethingRq tag

					foreach (QuickBooks_Utilities::listActions() as $action)
					{
						$request = QuickBooks_Utilities::actionToRequest($action);
						if (false !== strpos($xml, '<' . $request . ' '))
						{
							//$xml = str_replace('<' . $request . ' ', '<' . $request . ' requestID="' . $this->_constructRequestID($next['qb_action'], $next['ident']) . '" ', $xml);
							$xml = str_replace('<' . $request . ' ', '<' . $request . ' requestID="' . $next['quickbooks_queue_id'] . '" ', $xml);
							break;
						}
						else if (false !== strpos($xml, '<' . $request . '>'))
						{
							//$xml = str_replace('<' . $request . '>', '<' . $request . ' requestID="' . $this->_constructRequestID($next['qb_action'], $next['ident']) . '">', $xml);
							$xml = str_replace('<' . $request . '>', '<' . $request . ' requestID="' . $next['quickbooks_queue_id'] . '">', $xml);
							break;
						}
					}
				}
				else if ($this->_config['check_valid_requestid'])
				{
					// They embedded a requestID="..." attribute, let's make sure it's valid

					//$embedded_action = null;
					//$embedded_ident = null;
					//$this->_parseRequestID($requestID, $embedded_action, $embedded_ident);

					//if ($embedded_action != $next['qb_action'] or $embedded_ident != $next['ident'])
					if ($next['quickbooks_queue_id'] != $requestID)
					{
						// They are sending this request with an INVALID requestID! Error this out and warn them!

						$err = 'This request contains an invalid embedded requestID="..." attribute; either embed the $requestID parameter, or leave out the requestID="..." attribute entirely, found [' . $requestID . ' vs. expected ' . $next['quickbooks_queue_id'] . ']!';
					}
				}

				/*
				if ($this->_config['convert_unix_newlines'] and
					false === strpos($xml, "\r") and 				// there are currently no Windows newlines...
					false !== strpos($xml, "\n"))					// ... but there *are* Unix newlines!
				{
					; // (this is currently broken/unimplemented)
				}
				*/

				if ($err) // The function encountered an error when generating the qbXML request
				{
					//$this->_driver->errorLog($obj->ticket, QUICKBOOKS_ERROR_HANDLER, $err);
					//$this->_driver->log('ERROR: ' . $err, $obj->ticket, QUICKBOOKS_LOG_NORMAL);
					//$this->_driver->queueStatus($obj->ticket, $next['qb_action'], $next['ident'], QUICKBOOKS_STATUS_ERROR, 'Registered handler returned error: ' . $err);

					$errerr = '';
					//$this->_handleError($obj->ticket, QUICKBOOKS_ERROR_HANDLER, $err, $this->_constructRequestID($next['qb_action'], $next['ident']), $next['qb_action'], $next['ident'], $extra, $errerr, $xml);
					$this->_handleError($obj->ticket, QUICKBOOKS_ERROR_HANDLER, $err, $next['quickbooks_queue_id'], $next['qb_action'], $next['ident'], $extra, $errerr, $xml);

					return new QuickBooks_WebConnector_Result_SendRequestXML('');
				}
				else
				{
					//$this->_driver->log('Outgoing XML request: ' . $xml, $obj->ticket, QUICKBOOKS_LOG_DEBUG);
					$this->_log('Outgoing XML request: ' . $xml, $obj->ticket, QUICKBOOKS_LOG_DEBUG);

					if (strlen($xml) and  // Returned XML AND
						!$this->_extractRequestID($xml)) // Does not have a requestID in the request
					{
						// Mark it as successful right now
						//$this->_driver->queueStatus($obj->ticket, $next['qb_action'], $next['ident'], QUICKBOOKS_STATUS_SUCCESS, 'Unverified... no requestID attribute in XML stream.');
						$this->_driver->queueStatus($obj->ticket, $next['quickbooks_queue_id'], QUICKBOOKS_STATUS_SUCCESS, 'Unverified... no requestID attribute in XML stream.');
					}

					return new QuickBooks_WebConnector_Result_SendRequestXML($xml);
				}
			}
		}

		// Reporting an error, this will cause the QBWC to call ->getLastError()
		return new QuickBooks_WebConnector_Result_SendRequestXML('');
	}

	/**
	 * Extract the requestID attribute from an XML stream
	 *
	 * @param string $xml	The XML stream to look for a requestID attribute in
	 * @return mixed		The request ID
	 */
	protected function _extractRequestID($xml)
	{
		return QuickBooks_Utilities::extractRequestID($xml);
	}

	/**
	 * Create a requestID string from action and ident parts
	 *
	 * @param string $action
	 * @param mixed $ident
	 * @return string
	 */
	/*
	protected function _constructRequestID($action, $ident)
	{
		return QuickBooks_Utilities::constructRequestID($action, $ident);
	}
	*/

	/**
	 * Parse a requestID string into it's action and ident parts
	 *
	 * @param string $requestID
	 * @param string $action
	 * @param mixed $ident
	 * @return void
	 */
	/*
	protected function _parseRequestID($requestID, &$action, &$ident)
	{
		return QuickBooks_Utilities::parseRequestID($requestID, $action, $ident);
	}
	*/

	/**
	 * Extract a unique record identifier from an XML response
	 *
	 * Some (most?) records within QuickBooks have unique identifiers which are
	 * returned with the qbXML responses. This method will try to extract all
	 * identifiers it can find from a qbXML response and return them in an
	 * associative array.
	 *
	 * For example, Customers have unique ListIDs, Invoices have unique TxnIDs,
	 * etc. For an AddCustomer request, you'll get an array that looks like
	 * this:
	 * <code>
	 * array(
	 * 	'ListID' => '2C0000-1039887390'
	 * )
	 * </code>
	 *
	 * Other transactions might have more than one identifier. For instance, a
	 * call to AddInvoice returns both a ListID and a TxnID:
	 * <code>
	 * array(
	 * 	'ListID' => '200000-1036881887', // This is actually part of the 'CustomerRef' entity in the Invoice XML response
	 * 	'TxnID' => '11C26-1196256987', // This is the actual transaction ID for the Invoice XML response
	 * )
	 * </code>
	 *
	 * *** IMPORTANT *** If there are duplicate fields (i.e.: 3 different
	 * ListIDs returned) then only the first value encountered will appear in
	 * the associative array.
	 *
	 * The following elements/attributes are supported:
	 * 	- ListID
	 * 	- TxnID
	 * 	- iteratorID
	 * 	- OwnerID
	 * 	- TxnLineID
	 *
	 * @param string $xml	The XML stream to look for an identifier in
	 * @return array		An associative array mapping identifier fields to identifier values
	 */
	protected function _extractIdentifiers($xml)
	{
		$fetch_tagdata = array(
			'ListID',
			'TxnID',
			'OwnerID',
			'TxnLineID',
			'EditSequence',
			'FullName',
			'Name',
			'RefNumber',
			);

		$fetch_attributes = array(
			'requestID',
			'iteratorID',
			'iteratorRemainingCount',
			'metaData',
			'retCount',
			'statusCode',
			'statusSeverity',
			'statusMessage',
			'newMessageSetID',
			'messageSetStatusCode',
			);

		$list = array();

		foreach ($fetch_tagdata as $tag)
		{
			if (false !== ($start = strpos($xml, '<' . $tag . '>')) and
				false !== ($end = strpos($xml, '</' . $tag . '>')))
			{
				$list[$tag] = substr($xml, $start + 2 + strlen($tag), $end - $start - 2 - strlen($tag));
			}
		}

		foreach ($fetch_attributes as $attribute)
		{
			if (false !== ($start = strpos($xml, ' ' . $attribute . '="')) and
				false !== ($end = strpos($xml, '"', $start + strlen($attribute) + 3)))
			{
				$list[$attribute] = substr($xml, $start + strlen($attribute) + 3, $end - $start - strlen($attribute) - 3);
			}
		}

		return $list;
	}

	/**
	 * Extract the status code from an XML response
	 *
	 * Each qbXML response should return a status code and a status message
	 * indicating whether or not an error occured.
	 *
	 * @param string $xml	The XML stream to look for a response status code in
	 * @return integer		The response status code (0 if OK, another positive integer if an error occured)
	 */
	protected function _extractStatusCode($xml)
	{
		if (false !== ($start = strpos($xml, ' statusCode="')) and
			false !== ($end = strpos($xml, '"', $start + 13)))
		{
			return substr($xml, $start + 13, $end - $start - 13);
		}

		return QUICKBOOKS_ERROR_OK;
	}

	/**
	 * Extract the status message from an XML response
	 *
	 * Each qbXML response should return a status code and a status message
	 * indicating whether or not an error occured.
	 *
	 * @param string $xml	The XML stream to look for a response status message in
	 * @return string		The response status message
	 */
	protected function _extractStatusMessage($xml)
	{
		if (false !== ($start = strpos($xml, ' statusMessage="')) and
			false !== ($end = strpos($xml, '"', $start + 16)))
		{
			return substr($xml, $start + 16, $end - $start - 16);
		}

		return '';
	}

	/**
	 * Call the mapped function for a given action
	 *
	 * @param integer $which			Whether or call the request action handler (pass a 0) or the response action handler (pass a 1)
	 * @param string $user				QuickBooks username of the user the request/response is for
	 * @param string $action
	 * @param mixed $ident
	 * @param mixed $extra
	 * @param string $err				If the function returns an error message, the error message will be stored here
	 * @param integer $last_action_time
	 * @param integer $last_actionident_time
	 * @param string $xml				A qbXML response (if you're calling the response handler)
	 * @param array $qb_identifier
	 * @return string
	 */
	protected function _callMappedFunction($which, $user, $requestID, $action, $ident, $extra, &$err, $last_action_time, $last_actionident_time, $xml_or_version = '', $qb_identifier_or_locale = array(), $qbxml = null)
	{
		if ($which == 0)
		{
			return QuickBooks_Callbacks::callRequestHandler($this->_driver, $this->_map, $requestID, $action, $user, $ident, $extra, $err, $last_action_time, $last_actionident_time, $xml_or_version, $qb_identifier_or_locale, $this->_callback_config, $qbxml);
		}
		else if ($which == 1)
		{
			return QuickBooks_Callbacks::callResponseHandler($this->_driver, $this->_map, $requestID, $action, $user, $ident, $extra, $err, $last_action_time, $last_actionident_time, $xml_or_version, $qb_identifier_or_locale, $this->_callback_config, $qbxml);
		}

		$err = 'Request for a mapped function could not be fulfilled, invalid $which parameter.';
		return false;
	}

	/**
	 * Call a user-defined hook
	 *
	 * @param string $ticket
	 * @param string $hook
	 * @param string $requestID
	 * @param string $action
	 * @param mixed $ident
	 * @param mixed $extra
	 * @param string $err
	 * @param string $xml
	 * @param array $qb_identifiers
	 * @param array $hook_data
	 * @return boolean
	 */
	protected function _callHook($ticket, $hook, $requestID, $action, $ident, $extra, &$err, $xml = '', $qb_identifiers = array(), $hook_data = array())
	{
		$user = '';
		if ($ticket)
		{
			$user = $this->_driver->authResolve($ticket);
		}

		// Call the hook
		$ret = QuickBooks_Callbacks::callHook($this->_driver, $this->_hooks, $hook, $requestID, $user, $ticket, $err, $hook_data, $this->_callback_config, __FILE__, __LINE__);

		// If the hook reported an error, log the error
		if ($err)
		{
			$errerr = '';
			$this->_handleError($ticket, QUICKBOOKS_ERROR_HOOK, $err, $requestID, $action, $ident, $extra, $errerr, $xml, $qb_identifiers);
		}

		return true;
	}

	/**
	 * Call an error-handler function and update the status of a request to ERROR
	 *
	 * @param string $ticket
	 * @param integer $errnum		The error number from QuickBooks (see the QuickBooks SDK/IDN for a list of error codes)
	 * @param string $errmsg		The error message from QuickBooks
	 * @param string $requestID
	 * @param string $action
	 * @param mixed $ident
	 * @param array $extra
	 * @param string $err
	 * @param string $xml
	 * @param array $qb_identifiers
	 */
	protected function _handleError($ticket, $errnum, $errmsg, $requestID, $action, $ident, $extra, &$err, $xml = '', $qb_identifiers = array())
	{
		// , $requestID, $user, $action, $ident, $extra, &$err, $xml, $qb_identifier
		// Call the error handler (if one is set)

		$errmsg = html_entity_decode($errmsg);

		// First, set the status of the item to error
		/*
		if ($action and $ident)
		{
			$this->_driver->queueStatus($ticket, $action, $ident, QUICKBOOKS_STATUS_ERROR, $errnum . ': ' . $errmsg);
		}
		*/
		if ($requestID)
		{
			$this->_driver->queueStatus($ticket, $requestID, QUICKBOOKS_STATUS_ERROR, $errnum . ': ' . $errmsg);
		}

		// Log the last error (for the ticket)
		$this->_driver->errorLog($ticket, $errnum, $errmsg);
		//$this->_driver->log('Attempting to handle error: ' . $errnum . ', ' . $errmsg);
		$this->_log('Attempting to handle error: ' . $errnum . ', ' . $errmsg, $ticket, QUICKBOOKS_LOG_NORMAL);

		// By default, we don't want to continue if the error is not handled
		$continue = false;

		// Get username of user which experienced the error
		$user = $this->_driver->authResolve($ticket);

		// CALL THE ERROR HANDLER
		$err = '';
		$continue = QuickBooks_Callbacks::callErrorHandler($this->_driver, $this->_onerror, $errnum, $errmsg, $user, $requestID, $action, $ident, $extra, $err, $xml, $this->_callback_config);
		//													$Driver, $errmap, $errnum, $errmsg, $user, $action, $ident, $extra, &$errerr, $xml, $callback_config

		if ($err)
		{
			// Log error messages returned by the error handler
			//$this->_driver->log('An error occured while handling error: ' . $errnum . ': ' . $errmsg . ': ' . $err, $ticket, QUICKBOOKS_LOG_NORMAL);
			$this->_log('An error occured while handling error: ' . $errnum . ': ' . $errmsg . ': ' . $err, $ticket, QUICKBOOKS_LOG_NORMAL);
			$this->_driver->errorLog($ticket, QUICKBOOKS_ERROR_HANDLER, $err);
		}

		// Log the last error (for the log)
		//$this->_driver->log('Handled error: ' . $errnum . ': ' . $errmsg . ' (handler returned: ' . $continue . ')', $ticket, QUICKBOOKS_LOG_NORMAL);
		$this->_log('Handled error: ' . $errnum . ': ' . $errmsg . ' (handler returned: ' . $continue . ')', $ticket, QUICKBOOKS_LOG_NORMAL);

		// Update the queue status
		//if ($action and $ident)
		if ($requestID and
			$continue)
		{
			//$this->_driver->queueStatus($ticket, $action, $ident, QUICKBOOKS_STATUS_HANDLED, $errnum . ': ' . $errmsg);
			$this->_driver->queueStatus($ticket, $requestID, QUICKBOOKS_STATUS_HANDLED, $errnum . ': ' . $errmsg);
		}

		return $continue;
	}

	/**
	 * Calculate the current progress (what percentage done are we with this session?)
	 *
	 * @param string $ticket
	 * @return integer
	 */
	protected function _calculateProgress($ticket)
	{
		if ($this->_driver->authCheck($ticket)) // Check the ticket
		{
			$user = $this->_driver->authResolve($ticket);

			$current = $this->_driver->queueLeft($user);				// Number of items currently in the queue
			$processed = $this->_driver->queueProcessed($ticket);	// Number of items we've processed during this session

			$percentage = 100;
			if ($current)
			{
				$percentage = min(99, floor(100 * ($processed / ($processed + $current))));
			}

			// Call the percentage done hook
			$hookerr = null;
			$hookdata = array(
				'user' => $user,
				'percentage' => $percentage,
				'items_left' => $current,
				'items_processed' => $processed,
				);
			$this->_callHook($ticket, QUICKBOOKS_HANDLERS_HOOK_PERCENT, null, null, null, null, $hookerr, null, array(), $hookdata);

			if ($current)
			{
				return $percentage;
			}
			else
			{
				return 100;
			}
		}

		return -1;
	}

	/**
	 * ReceiveResponseXML() method for the QuickBooks Web Connector - Receive and handle a resonse form QuickBooks
	 *
	 * The stdClass object passed as a parameter will have the following members:
	 * 	- ->ticket 		The QuickBooks Web Connector ticket
	 * 	- ->response	An XML response message
	 * 	- ->hresult		Error code
	 * 	- ->message		Error message
	 *
	 * The sole data member of the returned object should be an integer.
	 * 	- The data member should be -1 if an error occured and QBWC should call ->getLastError()
	 * 	- Should be an integer 0 <= x < 100 to indicate success *and* that the application should continue to call ->sendRequestXML() at least one more time (more queued items still in the queue, the integer represents the percentage complete the total batch job is)
	 * 	- Should be 100 to indicate success *and* that the queue has been exhausted
	 *
	 * The following user-defined hooks are invoked:
	 * 	- QUICKBOOKS_HANDLERS_HOOK_RECEIVERESPONSEXML
	 *
	 * @param stdClass $obj
	 * @return QuickBooks_Result_ReceiveResponseXML
	 */
	public function receiveResponseXML($obj)
	{
		//$this->_driver->log('receiveResponseXML()', $obj->ticket, QUICKBOOKS_LOG_VERBOSE);
		$this->_log('receiveResponseXML()', $obj->ticket, QUICKBOOKS_LOG_VERBOSE);

		if ($this->_driver->authCheck($obj->ticket)) // Check the ticket
		{
			$user = $this->_driver->authResolve($obj->ticket);

			$hookdata = array(
				'username' => $user,
				'ticket' => $obj->ticket,
				);
			$hookerr = '';
			$this->_callHook($obj->ticket, QUICKBOOKS_HANDLERS_HOOK_RECEIVERESPONSEXML, null, null, null, null, $hookerr, null, array(), $hookdata);

			//$this->_driver->log('Incoming XML response: ' . $obj->response, $obj->ticket, QUICKBOOKS_LOG_DEBUG);
			$this->_log('Incoming XML response: ' . $obj->response, $obj->ticket, QUICKBOOKS_LOG_DEBUG);

			// Check if we got a error message...
			if (strlen($obj->message) or
				$this->_extractStatusCode($obj->response)) // or an error code
			{
				//$this->_log('Extracted code[' . $this->_extractStatusCode($obj->response) . ']', $obj->ticket, QUICKBOOKS_LOG_DEBUG);

				$action = null;
				$ident = null;
				$current = null;		// The current item we're receiving a response for

				$errnum = null;
				if ($requestID = $this->_extractRequestID($obj->response))
				{
					// This happens if a data validation error occurs
					//	(string too long, vendor name already taken, etc.)

					$errnum = $this->_extractStatusCode($obj->response);

					if ($current = $this->_driver->queueGet($user, $requestID, QUICKBOOKS_STATUS_PROCESSING))
					{
						// This is the particular item that experienced an error
						$action = $current['qb_action'];
						$ident = $current['ident'];
					}
					else
					{
						$requestID = null;
					}

					//$action = '';
					//$ident = '';
					//$this->_parseRequestID($requestID, $action, $ident);
				}
				else
				{
					// This happens if a protocol error occurs
					//	Poorly formed XML documents, missing XML node, missing line items, etc.)

					$errnum = $obj->hresult;

					// Try to guess at the request that caused an error (the last request that went out)
					if ($current = $this->_driver->queueProcessing($user))
					{
						$requestID = $current['quickbooks_queue_id'];
						$action = $current['qb_action'];
						$ident = $current['ident'];
					}
				}

				//if ($user and $action and $ident)
				if ($current)
				{
					// Fetch the request that was processed and EXPERIENCED AN ERROR!
					$extra = null;
					/*
					if ($current = $this->_driver->queueFetch($user, $action, $ident, QUICKBOOKS_STATUS_PROCESSING))
					{
						if ($current['extra'])
						{
							$extra = unserialize($current['extra']);
						}
					}
					*/

					if ($current['extra'])
					{
						$extra = unserialize($current['extra']);
					}

					$errmsg = null;
					if ($obj->message)
					{
						$errmsg = $obj->message;
					}
					else if ($status = $this->_extractStatusMessage($obj->response))
					{
						$errmsg = $status;
					}

					$errerr = '';
					$continue = $this->_handleError($obj->ticket, $errnum, $errmsg, $requestID, $action, $ident, $extra, $errerr, $obj->response, array());
					//					$errnum, $errmsg, $requestID, $action, $ident, $extra, &$err, $xml, $qb_identifiers = array()

					if ($errerr)
					{
						// The error handler returned an error too...
						//$this->_driver->log('An error occured while handling quickbooks error ' . $errnum . ': ' . $errmsg . ': ' . $errerr, $obj->ticket, QUICKBOOKS_LOG_NORMAL);
						$this->_log('An error occured while handling quickbooks error ' . $errnum . ': ' . $errmsg . ': ' . $errerr, $obj->ticket, QUICKBOOKS_LOG_NORMAL);
					}
				}
				else	// Generic error (poorly encoded XML, XML syntax error, etc.)
				{
					$errerr = '';
					$continue = $this->_handleError($obj->ticket, $obj->hresult, $obj->message, null, null, null, null, $errerr, $obj->response, array());

					if ($errerr)
					{
						// The error handler returned an error too...
						//$this->_driver->log('An error occured while handling generic error ' . $obj->hresult . ': ' . $obj->message . ': ' . $errerr, $obj->ticket, QUICKBOOKS_LOG_NORMAL);
						$this->_log('An error occured while handling generic error ' . $obj->hresult . ': ' . $obj->message . ': ' . $errerr, $obj->ticket, QUICKBOOKS_LOG_NORMAL);
					}
				}

				// Calculate the percentage done
				$progress = $this->_calculateProgress($obj->ticket);

				if (!$continue)
				{
					$progress = -1;
				}

				//$this->_driver->log('Transaction error at ' . $progress . '% complete... ', $obj->ticket, QUICKBOOKS_LOG_VERBOSE);
				$this->_log('Transaction error at ' . $progress . '% complete... ', $obj->ticket, QUICKBOOKS_LOG_VERBOSE);

				return new QuickBooks_WebConnector_Result_ReceiveResponseXML($progress);
			}

			$extra = null;
			$action = null;
			$ident = null;

			$requestID = null;
			if ($requestID = $this->_extractRequestID($obj->response) and
				$current = $this->_driver->queueGet($user, $requestID, QUICKBOOKS_STATUS_PROCESSING))
			{
				//$action = current(explode('|', $requestID));
				//$ident = end(explode('|', $requestID));
				/*
				$action = '';
				$ident = '';
				$this->_parseRequestID($requestID, $action, $ident);
				*/

				$action = $current['qb_action'];
				$ident = $current['ident'];

				// Fetch the request that's being processed
				$extra = null;
				/*
				if ($current = $this->_driver->queueFetch($user, $action, $ident, QUICKBOOKS_STATUS_PROCESSING))
				{
					if ($current['extra'])
					{
						$extra = unserialize($current['extra']);
					}
				}
				*/

				if ($current['extra'])
				{
					$extra = unserialize($current['extra']);
				}

				// Update the status to success (no error occured)
				//$this->_driver->queueStatus($obj->ticket, $action, $ident, QUICKBOOKS_STATUS_SUCCESS);
				$this->_driver->queueStatus($obj->ticket, $requestID, QUICKBOOKS_STATUS_SUCCESS);
			}
			else
			{
				// It's a good response... but we couldn't fetch the requestID for some reason?
				$this->_log('This appears to be a correct response, but the requestID could not be validated... ', $obj->ticket, QUICKBOOKS_LOG_VERBOSE);

				$progress = -1;

				return new QuickBooks_WebConnector_Result_ReceiveResponseXML($progress);
			}

			// Extract ListID, TxnID, etc. from the response
			$identifiers = $this->_extractIdentifiers($obj->response);

			//$this->_driver->log(var_export($identifiers, true), $obj->ticket, QUICKBOOKS_LOG_VERBOSE);

			// Auto-map $ident unique identifier from web application to the QuickBooks ListID or TxnID
			/*
			if ($this->_config['map_application_identifiers'])
			{
				$adds = QuickBooks_Utilities::listActions('*Add*');
				$mods = QuickBooks_Utilities::listActions('*Mod*');
				$qbkey = QuickBooks_Utilities::keyForAction($action);
				$type = QuickBooks_Utilities::actionToObject($action);

				$EditSequence = '';
				if (isset($identifiers['EditSequence']))
				{
					$EditSequence = $identifiers['EditSequence'];
				}

				if (in_array($action, $adds) and isset($identifiers[$qbkey]) and $type)
				{
					// Try to map the $ident to the QuickBooks identifier
					$this->_driver->identMap($user, $type, $ident, $identifiers[$qbkey], $EditSequence);
				}
				else if (in_array($action, $mods) and isset($identifiers[$qbkey]) and $type)
				{
					// Try to map the $ident to the QuickBooks identifier
					$this->_driver->identMap($user, $type, $ident, $identifiers[$qbkey], $EditSequence);
				}
			}
			*/

			$err = null;
			//$last_action_time = $this->_driver->queueActionLast($user, $action);
			//$last_actionident_time = $this->_driver->queueActionIdentLast($user, $action, $ident);
			$last_action_time = null;
			$last_actionident_time = null;

			//if ($ident) // If they didn't pass a requestID, $ident will not be set, and we can't call this reliably
			if ($requestID)
			{
				$this->_callMappedFunction(1, $user, $requestID, $action, $ident, $extra, $err, $last_action_time, $last_actionident_time, $obj->response, $identifiers);
			}

			// Calculate the percentage done
			$progress = $this->_calculateProgress($obj->ticket);

			if ($err)
			{
				$errerr = '';
				$continue = $this->_handleError($obj->ticket, QUICKBOOKS_ERROR_HANDLER, $err, $requestID, $action, $ident, $extra, $errerr, $obj->response, $identifiers);

				if (!$continue)
				{
					$progress = -1;
				}
			}

			//$this->_driver->log($progress . '% complete... ', $obj->ticket, QUICKBOOKS_LOG_VERBOSE);
			$this->_log($progress . '% complete... ', $obj->ticket, QUICKBOOKS_LOG_VERBOSE);

			return new QuickBooks_WebConnector_Result_ReceiveResponseXML($progress);
		}

		return new QuickBooks_WebConnector_Result_ReceiveResponseXML(-1);
	}

	/**
	 * QuickBooks Web Connector ->connectionError() SOAP method
	 *
	 * The stdClass object passed in as a parameter has these members:
	 * 	- ->ticket 		The ticket string
	 * 	- ->hresult		An error code
	 * 	- ->message 	An error message from QuickBooks/QBWC
	 *
	 * The following user-defined hooks are invoked:
	 * 	- QUICKBOOKS_HANDLERS_HOOK_CONNECTIONERROR
	 *
	 * @param stdClass $obj
	 * @return QuickBooks_Result_ConnectionError
	 */
	public function connectionError($obj)
	{
		//$this->_driver->log('connectionErrorXML()', $obj->ticket, QUICKBOOKS_LOG_VERBOSE);
		$this->_log('connectionErrorXML()', $obj->ticket, QUICKBOOKS_LOG_VERBOSE);

		if ($this->_driver->authCheck($obj->ticket))
		{
			$user = $this->_driver->authResolve($obj->ticket);

			$hookdata = array(
				'username' => $user,
				'ticket' => $obj->ticket,
				'hresult' => $obj->hresult,
				'message' => $obj->message,
				);
			$hookerr = '';
			$this->_callHook($obj->ticket, QUICKBOOKS_HANDLERS_HOOK_CONNECTIONERROR, null, null, null, null, $hookerr, null, array(), $hookdata);

			$err = '';
			$this->_handleError($obj->ticket, $obj->hresult, $obj->message, null, null, null, null, $err, null);

			return new QuickBooks_WebConnector_Result_ConnectionError('done');
		}

		return new QuickBooks_WebConnector_Result_ConnectionError('done');
	}

	/**
	 * QuickBooks Web Connector ->getLastError() SOAP method
	 *
	 * The stdClass object passed in as a parameter has these members:
	 * 	- ->ticket		The ticket string
	 *
	 * The returned object should have just one member, an error message
	 * describing the last error that occured.
	 *
	 * The following user-defined hooks are invoked:
	 * 	- QUICKBOOKS_HANDLERS_HOOK_GETLASTERROR
	 *
	 * @return QuickBooks_Result_GetLastError
	 */
	public function getLastError($obj)
	{
		//$this->_driver->log('getLastError()', $obj->ticket, QUICKBOOKS_LOG_VERBOSE);
		$this->_log('getLastError()', $obj->ticket, QUICKBOOKS_LOG_VERBOSE);

		if ($this->_driver->authCheck($obj->ticket))
		{
			$user = $this->_driver->authResolve($obj->ticket);

			$hookdata = array(
				'username' => $user,
				'ticket' => $obj->ticket,
				);
			$hookerr = '';
			$this->_callHook($obj->ticket, QUICKBOOKS_HANDLERS_HOOK_GETLASTERROR, null, null, null, null, $hookerr, null, array(), $hookdata);

			$lasterr = $this->_driver->errorLast($obj->ticket);

			return new QuickBooks_WebConnector_Result_GetLastError($lasterr);
		}

		return new QuickBooks_WebConnector_Result_GetLastError('Bad ticket.');
	}

	/**
	 * QuickBooks Web Connector ->closeConnection() SOAP method
	 *
	 * The stdClass object passed in as a parameter has these members:
	 * 	- ->ticket 		The ticket string
	 *
	 * The sole member of the returned object should be a string describing the reason for closing the connection
	 *
	 * @todo The "Complete!" message should probably be based on a configuration variable, user configurable
	 *
	 * The following user-defined hooks are invoked:
	 * 	- QUICKBOOKS_HANDLERS_HOOK_CLOSECONNECTION
	 *
	 * @return QuickBooks_Result_CloseConnection
	 */
	public function closeConnection($obj)
	{
		//$this->_driver->log('closeConnection()', $obj->ticket, QUICKBOOKS_LOG_VERBOSE);
		$this->_log('closeConnection()', $obj->ticket, QUICKBOOKS_LOG_VERBOSE);

		if ($this->_driver->authCheck($obj->ticket))
		{
			$user = $this->_driver->authResolve($obj->ticket);

			$hookdata = array(
				'username' => $user,
				'ticket' => $obj->ticket,
				);
			$hookerr = '';
			$this->_callHook($obj->ticket, QUICKBOOKS_HANDLERS_HOOK_CLOSECONNECTION, null, null, null, null, $hookerr, null, array(), $hookdata);

			//
			return new QuickBooks_WebConnector_Result_CloseConnection('Complete!');
		}

		// Bad ticket
		return new QuickBooks_WebConnector_Result_CloseConnection('Bad ticket.');
	}

	/**
	 * QuickBooks Web Connector ->getServerVersion() SOAP method
	 *
	 * The following user-defined hooks are invoked:
	 * 	- QUICKBOOKS_HANDLERS_HOOK_SERVERVERSION
	 *
	 * @param stdClass $obj
	 * @return QuickBooks_Result_GetServerVersion
	 */
	public function serverVersion($obj)
	{
		//$this->_driver->log('serverVersion()', '', QUICKBOOKS_LOG_VERBOSE);
		$this->_log('serverVersion()', '', QUICKBOOKS_LOG_VERBOSE);

		$hookdata = array(  );
		$hookerr = '';
		$this->_callHook(null, QUICKBOOKS_HANDLERS_HOOK_SERVERVERSION, null, null, null, null, $hookerr, null, array(), $hookdata);

		return new QuickBooks_WebConnector_Result_ServerVersion($this->_config['server_version']);
	}

	/**
	 * QuickBooks Web Connector ->clientVersion() SOAP method - Receive the QuickBooks Web Connector client version (and, if neccessary, act on it)
	 *
	 * This is an *optional* method, and not all versions of the QuickBooks Web
	 * Connector will support this method. It doesn't really even *need* to be
	 * implemented, but PHP will dump notices in our error log if we don't
	 * implement it and then the web connector tries to call it.
	 *
	 * The stdClass object passed as a parameter will have the following members:
	 * 	- strVersion	A string version code indicating the version of the QuickBooks Web Connector that's being used
	 *
	 * The one member of the returned object should be:
	 * 	- The empty string to tell the web connector to continue with the update
	 * 	- A string that begins with "W:" to display a warning message to the end-user, specify the warning message after the "W:"
	 * 	- A string that begins with "E:" to display an error message to the end-user, specify the error message after the "E:"
	 * 	- A string that begins with "O:" (as in OKAY) to tell the end-user that the server expects a newer version of the web connector, specify the minimum required version after the "O:"
	 *
	 * The following user-defined hooks are invovked:
	 * 	- QUICKBOOKS_HANDLERS_HOOK_CLIENTVERSION
	 *
	 * @param stdClass $obj
	 * @return QuickBooks_Result_ClientVersion
	 */
	public function clientVersion($obj)
	{
		//$this->_driver->log('clientVersion()', '', QUICKBOOKS_LOG_VERBOSE);
		$this->_log('clientVersion()', '', QUICKBOOKS_LOG_VERBOSE);

		$hookdata = array();
		$hookerr = '';
		$this->_callHook(null, QUICKBOOKS_HANDLERS_HOOK_CLIENTVERSION, null, null, null, null, $hookerr, null, array(), $hookdata);

		if (!is_null($this->_config['qbwc_min_version']))
		{
			if (version_compare($obj->strVersion, $this->_config['qbwc_min_version'], '<'))
			{
				//$this->_driver->log('Version Requirement, current: ' . $obj->strVersion . ', required: ' . $this->_config['qbwc_min_version'], '', QUICKBOOKS_LOG_NORMAL);
				$this->_log('Version Requirement, current: ' . $obj->strVersion . ', required: ' . $this->_config['qbwc_min_version'], '', QUICKBOOKS_LOG_NORMAL);

				return new QuickBooks_WebConnector_Result_ClientVersion('O:' . $this->_config['qbwc_min_version']);
			}
		}

		return new QuickBooks_WebConnector_Result_ClientVersion('');
	}

	/**
	 * QuickBooks Web Connector ->getInteractiveURL() SOAP method - Get the URL to use for an interactive session
	 *
	 * The stdClass object passed as a parameter will have the following members:
	 * 	- ticket		The ticket string
	 * 	- sessionID		??? (undocumented in QBWC documentation...?)
	 *
	 * The following user-defined hooks are invoked:
	 * 	- QUICKBOOKS_HANDLERS_HOOK_GETINTERACTIVEURL
	 *
	 * @param stdClass $obj
	 * @return QuickBooks_Result_GetInteractiveURL
	 */
	/*
	public function getInteractiveURL($obj)
	{
		//$this->_driver->log('getInteractiveURL()', $obj->ticket, QUICKBOOKS_LOG_VERBOSE);
		$this->_log('getInteractiveURL()', $obj->ticket, QUICKBOOKS_LOG_VERBOSE);

		if ($this->_driver->authCheck($obj->ticket))
		{
			$user = $this->_driver->authResolve($obj->ticket);

			$hookdata = array(
				'username' => $user,
				'ticket' => $obj->ticket,
				);
			$hookerr = '';
			$this->_callHook($obj->ticket, QUICKBOOKS_HANDLERS_HOOK_GETINTERACTIVEURL, null, null, null, null, $hookerr, null, array(), $hookdata);

			return new QuickBooks_Result_GetInteractiveURL($this->_config['qbwc_interactive_url']);
		}

		return new QuickBooks_Result_GetInteractiveURL('');
	}
	*/

	/**
	 *
	 * @todo Implement this... returned object is null!
	 *
	 * @param stdClass $obj
	 * @return void
	 */
	/*
	public function interactiveRejected($obj)
	{
		//$this->_driver->log('interactiveRejected()', $obj->ticket, QUICKBOOKS_LOG_VERBOSE);
		$this->_log('interactiveRejected()', $obj->ticket, QUICKBOOKS_LOG_VERBOSE);

		if ($this->_driver->authCheck($obj->ticket))
		{
			$user = $this->_driver->authResolve($obj->ticket);

			$hookdata = array(
				'username' => $user,
				'ticket' => $obj->ticket,
				);
			$hookerr = '';
			$this->_callHook($obj->ticket, QUICKBOOKS_HANDLERS_HOOK_GETINTERACTIVEURL, null, null, null, null, $hookerr, null, array(), $hookdata);

			return null;
		}

		return null;
	}
	*/

	/**
	 *
	 *
	 * The stdClass object passed as a parameter will have the following members:
	 * 	- ticket
	 *
	 * @param stdClass $obj
	 * @return QuickBooks_Result_InteractiveDone
	 */
	/*
	public function interactiveDone($obj)
	{
		//$this->_driver->log('interactiveDone()', $obj->ticket, QUICKBOOKS_LOG_VERBOSE);
		$this->_log('interactiveDone()', $obj->ticket, QUICKBOOKS_LOG_VERBOSE);

		if ($this->_driver->authCheck($obj->ticket))
		{
			$user = $this->_driver->authResolve($obj->ticket);

			$hookdata = array(
				'username' => $user,
				'ticket' => $obj->ticket,
				);
			$hookerr = '';
			$this->_callHook($obj->ticket, QUICKBOOKS_HANDLERS_HOOK_INTERACTIVEDONE, null, null, null, null, $hookerr, null, array(), $hookdata);

			return new QuickBooks_Result_InteractiveDone('Done');
		}

		return new QuickBooks_Result_InteractiveDone('');
	}
	*/
}


<?php

class Quickbooks extends CI_Model
{
	public function __construct()
	{
		parent::__construct();
	}

	/**
	 * Set the DSN connection string for the queue class
	 */
	public function dsn($dsn)
	{
		$this->_dsn = $dsn;
	}

	/**
	 * Queue up a request for the Web Connector to process
	 */
	public function enqueue($action, $ident, $priority = 0, $extra = null, $user = null)
	{
		$Queue = new QuickBooks_WebConnector_Queue($this->_dsn);

		return $Queue->enqueue($action, $ident, $priority, $extra, $user);
	}
}


<?php

/**
 *
 *
 * Copyright (c) 2010 Keith Palmer / ConsoliBYTE, LLC.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.opensource.org/licenses/eclipse-1.0.php
 *
 * @license LICENSE.txt
 * @author Keith Palmer <Keith@ConsoliBYTE.com>
 *
 * @package QuickBooks
 * @subpackage IPP
 */

QuickBooks_Loader::load('/QuickBooks/IPP/Service.php');

class QuickBooks_IPP_Service_Item extends QuickBooks_IPP_Service
{
	public function findAll($Context, $realmID, $query = null, $page = 1, $size = 50, $options = array())
	{
		return parent::_findAll($Context, $realmID, QuickBooks_IPP_IDS::RESOURCE_ITEM, $query, null, $page, $size, '', $options);
	}

	public function findById($Context, $realmID, $ID)
	{
		$xml = null;
		return parent::_findById($Context, $realmID, QuickBooks_IPP_IDS::RESOURCE_ITEM, $ID, $xml);
	}

	/**
	 * Find an item by name
	 *
	 * @param unknown_type $Context
	 * @param unknown_type $realmID
	 * @param unknown_type $name
	 */
	public function findByName($Context, $realmID, $name)
	{
		$IPP = $Context->IPP();

		if ($IPP->flavor() == QuickBooks_IPP_IDS::FLAVOR_DESKTOP)
		{
			for ($i = 0; $i < 999; $i++)
			{
				$list = $this->findAll($Context, $realmID, $name, $i, 50);

				foreach ($list as $Item)
				{
					if (strtolower($Item->getName()) == strtolower($name))
					{
						return $Item;
					}
				}
			}

			return false;
		}
		else
		{
			$xml = null;
			return parent::_findByName($Context, $realmID, QuickBooks_IPP_IDS::RESOURCE_ITEM, $name, $xml);
		}
	}

	public function add($Context, $realmID, $Object)
	{
		return parent::_add($Context, $realmID, QuickBooks_IPP_IDS::RESOURCE_ITEM, $Object);
	}

	public function update($Context, $realm, $IDType, $Object)
	{
		return parent::_update($Context, $realm, QuickBooks_IPP_IDS::RESOURCE_ITEM, $Object, $IDType);
	}

	public function delete($Context, $realmID, $IDType)
	{
		return parent::_delete($Context, $realmID, QuickBooks_IPP_IDS::RESOURCE_ITEM, $IDType);
	}

	public function query($Context, $realm, $query)
	{
		return parent::_query($Context, $realm, $query);
	}
}


<?php

class QuickBooks_XML_Backend_Builtin implements QuickBooks_XML_Backend
{
	protected $_xml;

	public function __construct($xml)
	{
		$this->_xml = $xml;
	}

	public function load($xml)
	{
		$this->_xml = $xml;

		return strlen($xml) > 0;
	}

	public function validate(&$errnum, &$errmsg)
	{
		if (!strlen($this->_xml))
		{
			return false;
		}

		$stack = array();
		$xml = $this->_xml;

		// Remove comments
		while (false !== strpos($xml, '<!--'))
		{
			$start = strpos($xml, '<!--');
			$end = strpos($xml, '-->', $start);

			if (false !== $start and false !== $end)
			{
				$xml = substr($xml, 0, $start) . substr($xml, $end + 3);
			}
			else
			{
				break;
			}
		}

		// Remove <![CDATA[ sections
		$has_cdata = false;
		while (false !== strpos($xml, '<![CDATA['))
		{
			$has_cdata = true;

			$start = strpos($xml, '<![CDATA[');
			$end = strpos($xml, ']]>', $start);

			if (false !== $start and false !== $end)
			{
				$xml = substr($xml, 0, $start) . substr($xml, $end + 3);
			}
			else
			{
				break;
			}
		}

		// Check well-formedness
		while (false !== strpos($xml, '<'))
		{
			$opentag_start = strpos($xml, '<');
			$opentag_end = strpos($xml, '>');

			$tag_w_attrs = trim(substr($xml, $opentag_start + 1, $opentag_end - $opentag_start - 1));

			$tag = '';
			$attributes = array();
			$this->_extractAttributes($tag_w_attrs, $tag, $attributes);

			if (substr($tag_w_attrs, 0, 1) == '?')			// < ? x m l
			{
				// ignore
			}
			else if (substr($tag_w_attrs, 0, 1) == '!')		// <!DOCTYPE
			{
				// ignore
			}
			else if (substr($tag_w_attrs, -1, 1) == '/')
			{
				// completely ignore, auto-closed because it has no children
			}
			else if (substr($tag_w_attrs, 0, 1) == '/')		// close tag
			{
				$tag = substr($tag, 1);

				$pop = array_shift($stack);

				if ($pop != $tag)
				{
					$errnum = QuickBooks_XML::ERROR_MISMATCH;
					$errmsg = 'Mismatched tags, found: ' . $tag . ', expected: ' . $pop;

					return false;
				}
			}
			else	// open tag
			{
				array_unshift($stack, $tag);
			}

			$xml = trim(substr($xml, $opentag_end + 1));
		}

		if (strlen($xml))
		{
			$errnum = QuickBooks_XML::ERROR_GARBAGE;
			$errmsg = 'Found this garbage data at end of stream: ' . $xml;
			return false;
		}

		if (count($stack))
		{
			$errnum = QuickBooks_XML::ERROR_DANGLING;
			$errmsg = 'XML stack still contains this after parsing: ' . var_export($stack, true);
			return false;
		}

		return true;
	}

	public function parse(&$errnum, &$errmsg)
	{
		$base = new QuickBooks_XML_Node('root');
		$this->_parseHelper($this->_xml, $base, $errnum, $errmsg);

		if ($errnum != QuickBooks_XML::ERROR_OK)
		{
			return false;
		}

		$tmp = $base->children();

		return new QuickBooks_XML_Document(current($tmp));
	}

	/**
	 * XML parsing recursive helper function
	 *
	 * @param string $xml
	 * @param QuickBooks_XML_Node $root
	 * @return void
	 */
	protected function _parseHelper($xml, &$Root, &$errnum, &$errmsg, $indent = 0)
	{
		$errnum = QuickBooks_XML::ERROR_OK;
		$errmsg = '';

		$arr = array();
		$xml = trim($xml);

		if (!strlen($xml))
		{
			return false;
		}

		$data = '';

		$vstack = array();
		$dstack = array();

		// Remove comments
		while (false !== strpos($xml, '<!--'))
		{
			$start = strpos($xml, '<!--');
			$end = strpos($xml, '-->', $start);

			if (false !== $start and false !== $end)
			{
				$xml = substr($xml, 0, $start) . substr($xml, $end + 3);
			}
			else
			{
				break;
			}
		}

		$raw = $xml;
		$current = 0;
		$last = '';

		$i = 0;

		// Parse
		while (false !== strpos($xml, '<'))
		{
			/*
			print('now examinging:');
			print('--------------');
			print($xml);
			print('-----------');
			print("\n\n\n");
			*/

			$opentag_start = strpos($xml, '<');
			$opentag_end = strpos($xml, '>');

			// CDATA check
			if (substr($xml, $opentag_start, 3) == '<![')
			{
				// Find the end of the CDATA section
				$cdata_end = strpos($xml, ']]>');

				$opentag_start = strpos($xml, '<', $cdata_end + 3);
				$opentag_end = strpos($xml, '>', $cdata_end + 3);
			}

			//print('opentag start/end (' . $opentag_start . ', ' . $opentag_end . ') puts us at: {{' . substr($xml, $opentag_start, $opentag_end - $opentag_start) . '}}' . "\n\n");

			$tag_w_attrs = trim(substr($xml, $opentag_start + 1, $opentag_end - $opentag_start - 1));

			$tag = '';
			$attributes = array();
			$this->_extractAttributes($tag_w_attrs, $tag, $attributes);

			if (substr($tag_w_attrs, 0, 1) == '?')		// xml declration
			{
				// ignore
			}
			else if (substr($tag_w_attrs, 0, 1) == '!')
			{
				// ignore
			}
			//else if (substr($tag_w_attrs, 0, 3) == '!--')		// comment
			//{
			//	// ignore
			//}
			else if (substr($tag_w_attrs, -1, 1) == '/')
			{
				// ***DO NOT*** completely ignore, auto-closed because it has no children
				// Completely ignoring causes some SOAP errors for requests like <serverVersion xmlns="http://developer.intuit.com/" />

				//print('TAG: [' . substr($tag_w_attrs, 0, -1 . ']' . "\n");
				//print('TWA: [' . $tag . ']' . "\n");

				//$tag_w_attrs = substr($tag_w_attrs, 0, -1);
				//$tag = substr($tag, 0, -1);

				$tag_w_attrs = rtrim($tag_w_attrs, '/');
				$tag = rtrim($tag, '/');

				// Shove the item on to the stack
				array_unshift($vstack, array( $tag, $tag_w_attrs, $current + $opentag_end ) );
				array_unshift($dstack, array( $tag, $tag_w_attrs, $current + $opentag_end ) );

				$key = key($vstack);
				$tmp = array_shift($vstack);

				$pop = $tag;
				$gnk = $tag_w_attrs;
				$pos = $current + $opentag_end;

				// there is no data, so empty data and the length is 0
				$length = 0;
				$data = null;

				if (count($vstack))
				{
					array_shift($dstack);
				}
				else
				{
					$dstack[$key] = array( $pop, $gnk, $pos, $length, $data );
				}
			}
			else if (substr($tag_w_attrs, 0, 1) == '/')		// close tag
			{
				// NOTE: If you change the code here, you'll likely have to
				//	change it in the above else () section as well, as that
				//	section handles data-less tags like <serverVersion />

				$tag = substr($tag, 1);

				$key = key($vstack);
				$tmp = array_shift($vstack);

				$pop = $tmp[0];
				$gnk = $tmp[1];
				$pos = $tmp[2];

				if ($pop != $tag)
				{
					$errnum = QuickBooks_XML::ERROR_MISMATCH;
					$errmsg = 'Mismatched tags, found: ' . $tag . ', expected: ' . $pop;

					return false;
				}

				$data = substr($raw, $pos, $current + $opentag_start - $pos);

				// Handle <![CDATA[ ... ]]> sections
				if (substr($data, 0, 9) == '<![CDATA[')
				{
					$cdata_end = strpos($data, ']]>');

					// Set the data to the CDATA section...
					$data = QuickBooks_XML::encode(substr($data, 9, $cdata_end - 9));

					// ... and remove the CDATA from the remaining XML string
					//$current = $current + strlen($data) + 12;
				}

				if (count($vstack))
				{
					array_shift($dstack);
				}
				else
				{
					$dstack[$key] = array( $pop, $gnk, $pos, $current + $opentag_start - $pos, $data );
				}
			}
			else	// open tag
			{
				array_unshift($vstack, array( $tag, $tag_w_attrs, $current + $opentag_end + 1 ) );
				array_unshift($dstack, array( $tag, $tag_w_attrs, $current + $opentag_end + 1 ) );
			}

			//print('stacks' . "\n");
			//print_r($vstack);
			//print_r($dstack);

			$xml = substr($xml, $opentag_end + 1);

			$current = $current + $opentag_end + 1;
		}

		if (strlen($xml))
		{
			$errnum = QuickBooks_XML::ERROR_GARBAGE;
			$errmsg = 'Found this garbage data at end of stream: ' . $xml;
			return false;
		}

		if (count($vstack))
		{
			$errnum = QuickBooks_XML::ERROR_DANGLING;
			$errmsg = 'XML stack still contains this after parsing: ' . var_export($vstack, true);
			return false;
		}

		//print_r($dstack);
		//exit;

		$dstack = array_reverse($dstack);

		$last = '';
		foreach ($dstack as $node)
		{
			$tag = $node[0];
			$tag_w_attrs = $node[1];
			$start = $node[2];

			if (count($node) < 5)
			{
				continue;
			}

			$length = $node[3];
			$payload = $node[4];

			$tmp = '';
			$attributes = array();
			$this->_extractAttributes($tag_w_attrs, $tmp, $attributes);

			$Node = new QuickBooks_XML_Node($tag);
			foreach ($attributes as $key => $value)
			{
				$value = QuickBooks_XML::decode($value, true);

				$Node->addAttribute($key, $value);
			}

			if (false !== strpos($payload, '<'))
			{
				// The tag contains child tags

				$tmp = $this->_parseHelper($payload, $Node, $errnum, $errmsg, $indent + 1);
				if (!$tmp)
				{
					return false;
				}
			}
			else
			{
				// This tag has no child tags contained inside it

				// Make sure we decode any entities
				$payload = QuickBooks_XML::decode($payload, true);

				$Node->setData($payload);
			}

			$Root->addChild($Node);

			$last = $tag;
		}

		return $Root;
	}

	protected function _extractAttributes($tag_w_attrs, &$tag, &$attributes)
	{
		$tag = '';
		$attributes = array();

		$tmp = QuickBooks_XML::extractTagAttributes($tag_w_attrs, true);

		$tag = array_shift($tmp);
		$attributes = $tmp;

		/*
		print('extracting attributes from: {{' . $tag_w_attrs . '}}' . "\n");
		print('	tag: [[' . $tag . ']]' . "\n");
		print('	attrs: ' . print_r($attributes, true) . "\n");
		print("\n");
		*/

		return true;
	}
}


<?php

/**
 *
 *
 * @author Keith Palmer <keith@consolibyte.com>
 * @license LICENSE.txt
 *
 * @package QuickBooks
 * @subpackage SQL
 */

/**
 *
 */
class QuickBooks_SQL
{
	/**
	 * Hook which occurs every time a new record is INSERTed into the SQL mirror
	 */
	const HOOK_SQL_INSERT = 'QuickBooks_SQL sql-insert';

	/**
	 * Hook which occurs every time a record is UPDATEd in the SQL mirror
	 */
	const HOOK_SQL_UPDATE = 'QuickBooks_SQL sql-update';

	/**
	 *
	 */
	const HOOK_SQL_DELETE = 'QuickBooks_SQL sql-delete';

	/**
	 *
	 */
	const HOOK_SQL_INVENTORY = 'QuickBooks_SQL sql-inventory';

	const HOOK_SQL_INVENTORYASSEMBLY = 'QuickBooks_SQL sql-inventoryassembly';

	/**
	 *
	 */
	const HOOK_QUICKBOOKS_INSERT = 'QuickBooks_SQL quickbooks-insert';

	/**
	 *
	 */
	const HOOK_QUICKBOOKS_UPDATE = 'QuickBooks_SQL quickbooks-update';

	/**
	 *
	 */
	const HOOK_QUICKBOOKS_DELETE = 'QuickBooks_SQL quickbooks-delete';

	/**
	 *
	 */
	protected $_config;

	/**
	 *
	 *
	 * @param string $dsn
	 * @param array $sql_options
	 * @param array $driver_options
	 */
	public function __construct($dsn, $sql_options = array(), $driver_options = array())
	{
		$this->_config = $this->_defaults($sql_options);
	}

	protected function _defaults($options)
	{
		$defaults = array(

			);

		return array_merge($defaults, $options);
	}

	/**
	 * Tell whether or not a string starts with another string
	 *
	 * @param string $str
	 * @param string $startswith
	 * @return boolean
	 */
	protected function _startsWith($str, $startswith)
	{
		$length = strlen($startswith);

		return (substr($str, 0, $length)) == $startswith;
	}

	/**
	 * Execute an SQL query and return the result resource
	 *
	 * @param string $sql		The SQL query to execute
	 * @param boolean $look		Whether or not to examine the query and see if it's an INSERT/UPDATE/DELETE query
	 * @return resource
	 */
	public function query($sql, $look = true)
	{
		if ($this->_driver)
		{
			if ($look)
			{
				$tmp = trim(strtoupper($sql));

				if ($this->_startsWith($sql, 'UPDATE '))
				{

				}
				else if ($this->_startsWith($sql, 'INSERT INTO '))
				{

				}
				else if ($this->_startsWith($sql, 'DELETE FROM '))
				{

				}
			}

			return $this->_driver->query($sql);
		}

		return false;
	}

	/**
	 * Fetch a record from a result resource
	 *
	 * @param resource $res			The result resource to fetch the next record from
	 * @param boolean $as_object
	 * @param integer $index
	 * @return object
	 */
	public function fetch($res, $as_object = true, $index = null)
	{

	}

	/**
	 *
	 *
	 * @param string $str
	 * @return string
	 */
	public function escape($str)
	{

	}

	public function last()
	{

	}

	public function getCustomer($listID)
	{

	}

	public function addCustomer($customer)
	{

	}

	public function modifyCustomer($listID, $customer)
	{

	}

	public function deleteCustomer($listID)
	{

	}
}


<?php

/**
 * File/class loader for QuickBooks packages
 *
 * Copyright (c) 2010 Keith Palmer / ConsoliBYTE, LLC.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.opensource.org/licenses/eclipse-1.0.php
 *
 * @package QuickBooks
 * @subpackage Loader
 */

//
if (!defined('QUICKBOOKS_LOADER_REQUIREONCE'))
{
	define('QUICKBOOKS_LOADER_REQUIREONCE', true);
}

if (!defined('QUICKBOOKS_LOADER_AUTOLOADER'))
{
	define('QUICKBOOKS_LOADER_AUTOLOADER', true);
}

/**
 *
 */
class QuickBooks_Loader
{
	/**
	 *
	 */
	static public function load($file, $autoload = true)
	{
		//print('loading file [' . $file . ']' . "\n");

		if ($autoload and
			QuickBooks_Loader::_autoload())
		{
			return true;
		}

		static $loaded = array();

		if (isset($loaded[$file]))
		{
			return true;
		}

		$loaded[$file] = true;

		if (QUICKBOOKS_LOADER_REQUIREONCE)
		{
			require_once QUICKBOOKS_BASEDIR . $file;
		}
		else
		{
			require QUICKBOOKS_BASEDIR . $file;
		}

		return true;
	}

	/**
	 *
	 */
	static protected function _autoload()
	{
		if (!QUICKBOOKS_LOADER_AUTOLOADER)
		{
			return false;
		}

		static $done = false;
		static $auto = false;

		if (!$done)
		{
			$done = true;

			if (function_exists('spl_autoload_register'))
			{
				// Register the autoloader, and return TRUE
				spl_autoload_register(array( 'QuickBooks_Loader', '__autoload' ));

				$auto = true;
				return true;
			}
		}

		return $auto;
	}

	/**
	 *
	 */
	static public function __autoload($name)
	{
		if (substr($name, 0, 10) == 'QuickBooks' && substr($name, 0, 16) != 'QuickBooksOnline')
		{
			$file = '/' . str_replace('_', DIRECTORY_SEPARATOR, $name) . '.php';
			QuickBooks_Loader::load($file, false);
		}
	}

	/**
	 * Import (require_once) a bunch of PHP files from a particular PHP directory
	 *
	 * @param string $dir
	 * @return boolean
	 */
	static public function import($dir, $autoload = true)
	{
		$dh = opendir(QUICKBOOKS_BASEDIR . $dir);
		if ($dh)
		{
			while (false !== ($file = readdir($dh)))
			{
				$tmp = explode('.', $file);
				if (end($tmp) == 'php' and
					!is_dir(QUICKBOOKS_BASEDIR . $dir . DIRECTORY_SEPARATOR . $file))
				{
					QuickBooks_Loader::load($dir . DIRECTORY_SEPARATOR . $file, $autoload);
					//require_once $dir . '/' . $file;
				}
			}

			return closedir($dh);
		}

		return false;
	}
}


<?php

/**
 * XML constants (and backward compat. class)
 *
 * Copyright (c) 2010-04-16 Keith Palmer / ConsoliBYTE, LLC.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.opensource.org/licenses/eclipse-1.0.php
 *
 * @package QuickBooks
 * @subpackage XML
 */

/**
 * Node class
 */
QuickBooks_Loader::load('/QuickBooks/XML/Node.php');

/**
 * Document class
 */
QuickBooks_Loader::load('/QuickBooks/XML/Document.php');

/**
 * XML parser
 */
QuickBooks_Loader::load('/QuickBooks/XML/Parser.php');

/**
 * XML backend interface
 */
QuickBooks_Loader::load('/QuickBooks/XML/Backend.php');

/**
 * XML parser backends
 */
QuickBooks_Loader::import('/QuickBooks/XML/Backend');

/**
 * QuickBooks XML base class
 */
class QuickBooks_XML
{
	/**
	 * Indicates an error *did not* occur
	 * @var integer
	 */
	const ERROR_OK = 0;

	/**
	 * Alias of QUICKBOOKS_XML_ERROR_OK
	 */
	const OK = 0;

	/**
	 * Indicates a tag mismatch/bad tag order
	 * @var integer
	 */
	const ERROR_MISMATCH = 1;

	/**
	 * Indicates garbage somewhere in the XML stream
	 * @var integer
	 */
	const ERROR_GARBAGE = 2;

	/**
	 * Indicates a bad XML entity
	 * @var integer
	 */
	const ERROR_ENTITY = 3;

	/**
	 * Indicates a dangling XML attribute after parsing
	 * @var integer
	 */
	const ERROR_DANGLING = 4;

	/**
	 * Internal XML parser error
	 * @var integer
	 */
	const ERROR_INTERNAL = 5;

	/**
	 * No content to parse error
	 * @var integer
	 */
	const ERROR_CONTENT = 6;

	/**
	 *
	 */
	const PARSER_BUILTIN = 'builtin';

	/**
	 *
	 */
	const PARSER_SIMPLEXML = 'simplexml';

	/**
	 *
	 * $xml = '
	 * 	<Person>
	 * 		<Name type="firstname">Keith</Name>
	 * 	</Person>
	 * ';
	 *
	 * $arr = array(
	 * 	'Person' => array(
	 * 		'Name' => 'Keith',
	 * 		),
	 * 	);
	 *
	 */
	const ARRAY_NOATTRIBUTES = 'no-attrs';

	/**
	 *
	 *
	 * $arr = array(
	 * 	'Person' => array(
	 * 		'Name' => 'Keith',
	 * 		'Name_type' => 'firstname',
	 * 		),
	 * 	);
	 *
	 *
	 */
	const ARRAY_EXPANDATTRIBUTES = 'child-attrs';

	/**
	 *
	 * $arr = array(
	 * 	0 => array(
	 * 		'name' => 'Person',
	 * 		'attributes' => array( ),
	 * 		'children' => array(
	 * 			0 => array(
	 * 				'name' => 'Name',
	 * 				'attributes' => array(
	 * 					'type' => 'firstname',
	 * 				),
	 * 				'children' => array(  ),
	 * 				'data' => 'Keith',
	 * 			),
	 * 		),
	 * 		'data' => null,
	 * 	),
	 * );
	 *
	 */
	const ARRAY_BRANCHED = 'branched';

	/**
	 *
	 *
	 * $arr = array(
	 * 	'Person Name' => 'Keith',
	 * 	);
	 *
	 *
	 */
	const ARRAY_PATHS = 'paths';

	/**
	 * Flag to compress empty XML elements
	 *
	 * <Customer>
	 * 	<FirstName>Keith</FirstName>
	 * 	<LastName />
	 * </Customer>
	 *
	 * @note Defined as an integer for backwards compat.
	 * @var integer
	 */
	const XML_COMPRESS = 1;

	/**
	 * Flag to drop empty XML elements
	 *
	 * <Customer>
	 * 	<FirstName>Keith</FirstName>
	 * </Customer>
	 *
	 * @note Defined as an integer for backwards compat.
	 * @var integer
	 */
	const XML_DROP = -1;

	/**
	 * Flag to preserve empty elements
	 *
	 * <Customer>
	 * 	<FirstName>Keith</FirstName>
	 * 	<LastName></LastName>
	 * </Customer>
	 *
	 * @note Defined as an integer for backwards compat.
	 * @var integer
	 */
	const XML_PRESERVE = 0;

	/**
	 * Extract the contents from a particular XML tag in an XML string
	 *
	 *
	 * $xml = '<document><stuff>bla bla</stuff><other>ble ble</other></document>';
	 * $contents = QuickBooks_Utilities::_extractTagContents('stuff', $xml);
	 * print($contents); 	// prints "bla bla"
	 *
	 *
	 * @param string $tag		The XML tag to extract the contents from
	 * @param string $data		The XML document
	 * @return string			The contents of the tag
	 */
	static public function extractTagContents($tag, $data)
	{
		$tag = trim($tag, '<> ');

		if (false !== strpos($data, '<' . $tag . '>') and
			false !== strpos($data, '</' . $tag . '>'))
		{
			$data = strstr($data, '<' . $tag . '>');
			$end = strpos($data, '</' . $tag . '>');

			return substr($data, strlen($tag) + 2, $end - (strlen($tag) + 2));
		}

		return null;
	}

	// @todo Documentation
	static public function extractTagAttribute($attribute, $tag_w_attrs, $which = 0)
	{
		/*
		if (false !== ($start = strpos($tag_w_attrs, $attribute . '="')) and
			false !== ($end = strpos($tag_w_attrs,  '"', $start + strlen($attribute) + 2)))
		{
			return substr($tag_w_attrs, $start + strlen($attribute) + 2, $end - $start - strlen($attribute) - 2);
		}

		return null;
		*/

		$attr = $attribute;
		$data = $tag_w_attrs;

		if ($which == 1)
		{
			$spos = strpos($data, $attr . '="');
			$data = substr($data, $spos + strlen($attr));
		}

		if (false !== ($spos = strpos($data, $attr . '="')) and
			false !== ($epos = strpos($data, '"', $spos + strlen($attr) + 2)))
		{
			//print('start: ' . $spos . "\n");
			//print('end: ' . $epos . "\n");

			return substr($data, $spos + strlen($attr) + 2, $epos - $spos - strlen($attr) - 2);
		}

		return '';
	}

	/**
	 * Extract the attributes from a tag container
	 *
	 * @todo Holy confusing code Batman!
	 *
	 * @param string $tag_w_attributes
	 * @param string $tag
	 * @param array $attributes
	 * @return void
	 */
	static public function extractTagAttributes($tag_w_attrs, $return_tag_first = false)
	{
		$tag = '';
		$attributes = array();

		$tag_w_attrs = trim($tag_w_attrs);

		/*if (substr($tag_w_attrs, -1, 1) == '/')		// condensed empty tag
		{
			$tag = trim($tag_w_attrs, '/ ');
			$attributes = array();
		}
		else*/
		if (false !== strpos($tag_w_attrs, ' '))
		{
			$tmp = explode(' ', $tag_w_attrs);
			//$tag = trim(array_shift($tmp), " \n\r\t<>");
			$tag = trim(array_shift($tmp));

			$attributes = array();

			$attrs = trim(implode(' ', $tmp));
			$length = strlen($attrs);

			$key = '';
			$value = '';
			$in_key = true;
			$in_value = false;
			$expect_key = false;
			$expect_value = false;

			for ($i = 0; $i < $length; $i++)
			{
				if ($attrs{$i} == '=')
				{
					$in_key = false;
					$in_value = false;
					$expect_value = true;
				}
				/*
				else if ($attrs{$i} == '"' and $expect_value)
				{
					$in_value = true;
					$expect_value = false;
				}
				*/
				/*else if ($attrs{$i} == '"' and $in_value)*/
				else if (($attrs{$i} == '"' or $attrs{$i} == '\'') and $expect_value)
				{
					$in_value = true;
					$expect_value = false;
				}
				else if (($attrs{$i} == '"' or $attrs{$i} == '\'') and $in_value)
				{
					$attributes[trim($key)] = $value;

					$key = '';
					$value = '';

					$in_value = false;
					$expect_key = true;
				}
				else if ($attrs{$i} == ' ' and $expect_key)
				{
					$expect_key = false;
					$in_key = true;
				}
				else if ($in_key)
				{
					$key .= $attrs{$i};
				}
				else if ($in_value)
				{
					$value .= $attrs{$i};
				}
			}

			/*
			foreach ($tmp as $attribute)
			{
				if (false !== ($pos = strpos($attribute, '=')))
				{
					$key = trim(substr($attribute, 0, $pos));
					$value = trim(substr($attribute, $pos + 1), '"');

					$attributes[$key] = $value;
				}
			}*/
		}
		else
		{
			$tag = $tag_w_attrs;
			$attributes = array();
		}

		// This returns the actual tag without attributes as the first key of the array
		if ($return_tag_first)
		{
			array_unshift($attributes, $tag);
		}

		return $attributes;
	}

	/**
	 * Encode a string for use within an XML document
	 *
	 * @todo Investigate QuickBooks qbXML encoding and implement solution
	 *
	 * @param string $str				The string to encode
	 * @param boolean $for_qbxml
	 * @return string
	 */
	static public function encode($str, $for_qbxml = true, $double_encode = true)
	{
		$transform = array(
			'&' => '&',
			'<' => '<',
			'>' => '>',
			//'\'' => ''',
			'"' => '"',
			);

		$str = str_replace(array_keys($transform), array_values($transform), $str);

		if (!$double_encode)
		{
			$fix = array();
			foreach ($transform as $raw => $encoded)
			{
				$fix[str_replace('&', '&', $encoded)] = $encoded;
			}

			$str = str_replace(array_keys($fix), array_values($fix), $str);
		}

		return $str;
	}

	/**
	 * Decode a string for use within an XML document
	 *
	 * @todo Investigate QuickBooks qbXML encoding and implement solution
	 *
	 * @param string $str				The string to encode
	 * @param boolean $for_qbxml
	 * @return string
	 */
	static public function decode($str, $for_qbxml = true)
	{
		$transform = array(
			'<' => '<',
			'>' => '>',
			''' => '\'',
			'"' => '"',
			'&' => '&', 		// Make sure that this is *the last* transformation to run, otherwise we end up double-un-encoding things
			);

		return str_replace(array_keys($transform), array_values($transform), $str);
	}
}