diff -Nup ./qb.patch ../../modules/qb/qb.patch --- ./qb.patch 2008-08-21 18:13:18.000000000 +0900 +++ ../../modules/qb/qb.patch 1970-01-01 09:00:00.000000000 +0900 @@ -1,152 +0,0 @@ -diff -Nup ./qb.inc ../../modules/qb/qb.inc ---- ./qb.inc 2008-08-21 18:09:41.000000000 +0900 -+++ ../../modules/qb/qb.inc 2008-07-23 11:56:49.000000000 +0900 -@@ -1,15 +1 @@ -- 'textfield', -- '#title' => t('Company file'), -- '#description' => t('The full path to your quickbooks company file, e.g. C:\Data\MyCompany\MyCompany.qdb. If you leave this blank, applications will use the file that is currently open on your workstation.'), -- ); -- // Make sure the buttons show up someplace responsible. -- $form['buttons'] = array('#weight' => 10); -- -- return system_settings_form($form); --} -+ array( -- 'title' => t('Quickbooks'), -- 'page callback' => 'drupal_get_form', -- 'page arguments' => array('qb_settings'), -- 'access arguments' => array('administer quickbooks'), -- 'file' => 'qb.inc', -- 'type' => MENU_NORMAL_ITEM, -- ), -- ); -+function qb_menu($may_cache = true) { -+ $items = array(); -+ -+ if ($may_cache) { -+ $items[] = array( -+ 'path' => 'admin/settings/qb', -+ 'title' => t('Quickbooks Settings'), -+ 'callback' => 'drupal_get_form', -+ 'callback arguments' => array('qb_settings'), -+ 'access' => array('administer quickbooks') -+ ); -+ } -+ -+ return $items; - } - - function qb_perm() { - return array('administer quickbooks'); - } - --function qb_qbxml($root_entity, $elements) { -- $qbxml = new DOMDocument('1.0'); -- $xml = $qbxml->appendChild($qbxml->createElement($root_entity)); -- foreach ($elements as $name => $value) { -- _qb_qbxml_node($qbxml, $name, $value, $xml); -+/** -+ * Menu callback to set module settings -+ */ -+function qb_settings() { -+ $form = array(); -+ -+ $form['qb_company_file'] = array( -+ '#type' => 'textfield', -+ '#title' => t('Company file'), -+ '#description' => t('The full path to your quickbooks company file, e.g. C:\Data\MyCompany\MyCompany.qdb. If you leave this blank, applications will use the file that is currently open on your workstation.'), -+ '#default_value' => variable_get('qb_company_file', '') -+ ); -+ -+ return system_settings_form($form); -+} -+ -+function qb_qbxml($elements, $root = array('QBXML')) { -+ $qbxml = $xml = new DOMDocument('1.0'); -+ -+ // Make sure the root is an array so _qb_qbxml_node can process it properly -+ if (!is_array($root)) { -+ $root = array($root => array()); - } -+ -+ // Attach the base/common root of the document -+ $start = _qb_qbxml_node($qbxml, key($root), current($root), $xml); -+ -+ // Attach the rest of the document -+ foreach((array)$elements as $key => $element) { -+ _qb_qbxml_node($qbxml, $key, $element, $start); -+ } -+ - return $qbxml->saveXML(); - } - - function _qb_qbxml_node(&$doc, $name, $value, &$parent) { -- if (is_string($value)) { -+ // Store the elements in the order they were added to the doc -+ static $added; -+ -+ if (is_scalar($value)) { - $element = $doc->createElement($name, $value); -+ $parent->appendChild($element); - } - if (is_array($value)) { -- $element = $doc->createElement($name); -- foreach ($value as $k => $v) { -- _qb_qbxml_node($doc, $k, $v, $element); -+ if (is_numeric(key($value))) { -+ for($k=0;$v=$value[$k];$k++) { -+ _qb_qbxml_node($doc, $name, $v, $parent); -+ } -+ } -+ else { -+ $element = $doc->createElement($name); -+ foreach ($value as $k => $v) { -+ // Denote attributes as starting with a _ -+ if (is_scalar($v) && strpos($k, '_') === 0) { -+ $k = substr($k, 1); -+ $attr = $doc->createAttribute($k); -+ $attr->appendChild($doc->createTextNode($v)); -+ $element->appendChild($attr); -+ } -+ // Denote processing instructions with a ? -+ else if (is_scalar($v) && strpos($k, '?') === 0) { -+ $k = substr($k, 1); -+ $doc->appendChild($doc->createProcessingInstruction($k, $v)); -+ } -+ else { -+ _qb_qbxml_node($doc, $k, $v, $element); -+ } -+ } -+ $added[] = $parent->appendChild($element); - } - } -- $parent->appendChild($element); -+ -+ // Try and return the last added element (first in the array because of recursion) to help have a more complex "root" as specified in qb_qbxml -+ return reset($added); - } diff -Nup ./qbwc.inc ../../modules/qb/qbwc.inc --- ./qbwc.inc 2008-08-21 18:09:41.000000000 +0900 +++ ../../modules/qb/qbwc.inc 2008-08-21 12:06:36.000000000 +0900 @@ -1,30 +1,34 @@ close_ts) { - return false; + $uid = db_result(db_query('SELECT uid FROM {sessions} WHERE sid = "%s" AND hostname = "%s"', $ticket, $_SERVER['REMOTE_ADDR'])); + if ($uid && $ticket != session_id()) { + global $user; + $user = user_load(array('uid' => $user)); + + session_destroy(); + session_id($ticket); + // Pretend that cookies are enabled so we can get back our session + $_COOKIE[session_name()] = true; + session_set_save_handler('sess_open', 'sess_close', 'sess_read', 'sess_write', 'sess_destroy_sid', 'sess_gc'); + session_start(); + } + else if (!$uid) { + // Invalid session + exit; } - - // Switch to the Quickbooks user. - if ($user->uid == 0) $user = user_load(array('uid' => $session->uid)); - - return $session; ; } function authenticate($params) { $name = $params->strUserName; $pass = $params->strPassword; + $hostname = $_SERVER['REMOTE_ADDR']; - $hostname = ip_address(); - - // Generate a ticket to be used for all further communication - $ticket = md5(time() . $name); + // Use the current session id as the ticket + $ticket = session_id(); // Default result: "non-valid user credentials". $ret = 'nvu'; @@ -35,31 +39,36 @@ class qbwc { // Authenticate as the quickbooks drupal user if ($name && ($name == variable_get('qbwc_user', ''))) { - if ($user = user_authenticate(array('name' => $name, 'pass' => $pass))) { - - // Return company file or empty, which means "currently open file". - $ret = variable_get('qb_company_file', ''); - - if ($theres_nothing_to_do) { // TODO - $ret = 'none'; + if ($user = user_authenticate($name, $pass)) { + // Set nothing to do until a module decides it has something to do + $ret = 'none'; + + // Let modules return whether they have work to do or not + // If no modules return anything, assume no work + foreach(module_implements('qbwc_authenticate') as $module) { + if (module_invoke($module, 'qbwc_authenticate', $ticket, $user)) { + // Return company file or empty, which means "currently open file" when work has been set + $ret = variable_get('qb_company_file', ''); + } } } } - // Close the session immediately if the authentication failed. - $close_ts = ($ret == 'nvu' ) ? time() : 'NULL'; - db_query("INSERT INTO {qbwc_sessions} ( ticket, hostname, uid, open_ts, close_ts ) VALUES ( '%s', '%s', %d, %d, $close_ts )", $ticket, $hostname, $user->uid, time()); - $result = array($ticket, $ret, $next_update, $minimum_wait); return (object) array('authenticateResult' => $result); } function closeConnection($params) { $ticket = $params->ticket; + $this->_session($ticket); global $user; + + // Let modules know we are closing the session + module_invoke_all('qbwc_close', $ticket); // Close the session and log it just like user_logout() sans drupal_goto. - watchdog('user', 'Session closed for %name.', array('%name' => $user->name)); + watchdog('user', t('Session closed for %name.', array('%name' => $user->name))); + // Destroy the current session. session_destroy(); module_invoke_all('user', 'logout', NULL, $user); @@ -67,12 +76,12 @@ class qbwc { // Load the anonymous user. $user = drupal_anonymous_user(); - db_query("UPDATE {qbwc_sessions} SET close_ts = %d where ticket = '%s'", time(), $ticket); - return (object) array( 'closeConnectionResult' => t('Session closed')); } function connectionError($ticket, $hresult, $message) { + // Do not try to reconnect + return 'done'; } function getLastError($ticket) { @@ -84,62 +93,140 @@ class qbwc { } function sendRequestXML($params) { + static $requestId = 1; + $ticket = $params->ticket; $HCPResponse = $params->strHCPResponse; $company_file = $params->strCompanyFileName; $country = $params->qbXMLCountry; $major_version = $params->qbXMLMajorVers; $minor_version = $params->qbXMLMinorVers; - - $session = $this->_session($ticket); + + $this->_session($ticket); // Default: If the web service has no requests to send, use an empty string. $ret = ''; // Decide if we have anything to do and return qbxml. - if($requests = module_invoke_all('qbwc_request', $session)) { - foreach ( $requests as $r ) { - $ret .= qb_qbxml($r['name'], $r['request']); + $requests = array(); + $_SESSION['requestIds'] = $_SESSION['savedData'] = array(); + + foreach(module_implements('qbwc_request') as $module) { + $request = module_invoke($module, 'qbwc_request'); + + // Make sure we have an array of requests instead of just a request + if (is_array($request) && !is_numeric(key($request))) { + $request = array($request); + } + + foreach((array)$request as $r) { + // Set data that module response/request handlers would like to pass between them + // during calls to QB + if (isset($r['data'])) { + $_SESSION['savedData'][$requestId] = $r['data']; + } + + // Attach request ids so we know how to send responses back to the right modules + // If modules set it to their own, we'll have to leave it up to them to avoid conflicts for now' + if (!isset($r['_requestId'])) { + $r['request']['_requestID'] = $requestId; + $_SESSION['requestIds'][$requestId++] = $module; + } + + if ($r['name']) { + $requests[$r['name']][] = $r['request']; + } } } + // Formulate the common base XML for all QBWC requests, using QBXML v5.0 + $top = array( + 'QBXML' => array( + '?qbxml' => 'version="5.0"', // XML processing instruction + 'QBXMLMsgsRq' => array( + '_onError' => 'stopOnError' // XML attribute + ) + ) + ); + + $ret = qb_qbxml($requests, $top); + return (object) array( 'sendRequestXMLResult' => $ret ); } function receiveResponseXML($params) { $ticket = $params->ticket; - $response = $params->response; + $response = DOMDocument::loadXML($params->response); $hresult = $params->hresult; $message = $params->message; + $this->_session($ticket); + // Get the list of data stored for this response + $data = $_SESSION['savedData']; + + // Split out the response elements so they can be sent to the appropriate modules + $elements = array(); + if (is_object($response)) { + $top = $response->getElementsByTagName('QBXMLMsgsRs')->item(0); + for($i=0;$element = $top->childNodes->item($i); $i++) { + if ($element->nodeType == 3) { + // Skip #text node types + continue; + } + $requestId = $element->getAttribute('requestID'); + if ($module = $_SESSION['requestIds'][$requestId]) { + $elements[$module][] = $element; + + // Set the saved data for this modules requests + if ($data[$requestId]) { + $saved[$module][$requestId] = $data[$requestId]; + } + } + } + } - $session = $this->_session($ticket); - - /* A positive integer from 1-100 represents the percentage of work - completed, where value of 100 means there is no more work. A negative - value means an error has occurred and Web Connector calls getLastError. - */ - if($responses = module_invoke_all('qbwc_response', $session, $response, $hresult, $message)) { - $ret = 0; - foreach ($responses as $item) { - $ret += $item; + $done = 0; + $total = 0; + foreach(module_implements('qbwc_response') as $module) { + $ret = module_invoke($module, 'qbwc_response', $elements[$module], $hresult, $message, $saved[$module]); + // Only count returned values less than 100 + if (is_numeric($ret) && $ret <= 100) { + /* A positive integer from 1-100 represents the percentage of work + completed, where value of 100 means there is no more work. A negative + value means an error has occurred and Web Connector calls getLastError. + */ + $done += $ret; + $total++; } - $ret = $ret / count($responses); } - if (!isset($ret)) $ret = 100; + // Put the accumulation of returned percentages with the total into one total percentage + if ($total) { + $done = $done / $total; + } + else { + $done = 100; + } + + // If it runs this long, something is probably wrong... + if ($_SESSION['countTimes']++ == 30) { + $done = 100; + } - return (object) array( 'receiveResponseXMLResult' => $ret ); + return (object) array( 'receiveResponseXMLResult' => $done ); } - /* Unimplemented functions + // Unimplemented functions function getInteractiveURL($ticket, $session_id) { + $this->_session($ticket); } function interactiveDone($ticket) { + $this->_session($ticket); } function interactiveRejected($ticket, $reason) { + $this->_session($ticket); } @@ -147,6 +234,4 @@ class qbwc { // the client's version. It could be used to set a minimum version level. function clientVersion($version) { } - - */ } diff -Nup ./qbwc.info ../../modules/qb/qbwc.info --- ./qbwc.info 2008-08-21 18:09:41.000000000 +0900 +++ ../../modules/qb/qbwc.info 2008-07-23 18:00:14.000000000 +0900 @@ -1,5 +1,3 @@ -; $Id: qbwc.info,v 1.1 2008/06/13 00:29:38 vauxia Exp $ name = Web Connector description = Implements a Quickbooks Web Connector server -core = 6.x -dependencies[] = qb +dependencies = qb diff -Nup ./qbwc.install ../../modules/qb/qbwc.install --- ./qbwc.install 2008-08-21 18:09:41.000000000 +0900 +++ ../../modules/qb/qbwc.install 2008-08-20 06:19:47.000000000 +0900 @@ -1,30 +1,16 @@ - array( - 'fields' => array( - 'ticket' => array('type' => 'varchar', 'length' => 64, 'not null' => TRUE), - 'uid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE), - 'hostname' => array('type' => 'varchar', 'length' => 15), - 'open_ts' => array('type' => 'int', 'not null' => TRUE), - 'close_ts' => array('type' => 'int', 'default' => 0), - ), - 'indexes' => array( - 'ticket' => array('ticket'), - ), - ), - ); -} + t('Quickbooks Web Connector'), - 'page callback' => 'qbwc', - 'access arguments' => array('access qbwc'), - 'type' => MENU_CALLBACK, - ); - $items['admin/settings/qb/qwc'] = array( - 'title' => t('QWC File'), - 'page callback' => 'qbwc_qwc', - 'access arguments' => array('access qbwc'), - 'type' => MENU_CALLBACK, - ); + if ($may_cache) { + $items[] = array( + 'title' => t('Quickbooks Web Connector'), + 'path' => 'qbwc', + 'callback' => 'qbwc', + 'type' => MENU_CALLBACK, + 'access' => true + ); + $items[] = array( + 'title' => t('QWC File'), + 'path' => 'admin/settings/qb/qwc', + 'callback' => 'qbwc_qwc', + 'access' => user_access('access qbwc'), + 'type' => MENU_CALLBACK, + ); + $items[] = array( + 'title' => t('Quickbooks Web Connector Setup'), + 'path' => 'admin/settings/qb/qbwc', + 'callback' => 'drupal_get_form', + 'callback arguments' => array('qbwc_settings'), + 'access' => user_access('access qbwc'), + 'type' => MENU_LOCAL_TASK + ); + } return $items; } @@ -23,67 +35,70 @@ function qbwc_perm() { return array('access qbwc'); } -function qbwc_form_alter(&$form, $form_state, $form_id) { - // Add QBWC settings to the Quickbooks settings page - if ($form_id == 'qb_settings') { - $form['qbwc'] = array( - '#title' => t('Web Connector settings'), - '#type' => 'fieldset', - '#collapsible' => TRUE - ); +function qbwc_settings() { + $form['qbwc'] = array( + '#title' => t('Web Connector settings'), + '#type' => 'fieldset', + '#collapsible' => TRUE + ); - $form['qbwc']['qbwc_url'] = array( - '#type' => 'textfield', - '#title' => t('Web Connector URL'), - '#description' => t('The URL the Web Connector will use to communicate with your Drupal site. Please note that it MUST be SSL if the hostname is not "localhost"'), - '#default_value' => variable_get('qbwc_url', str_replace('http:', 'https:', url('qbwc', array('absolute' => TRUE)))), - ); + $form['qbwc']['qbwc_url'] = array( + '#type' => 'textfield', + '#title' => t('Web Connector URL'), + '#description' => t('The URL the Web Connector will use to communicate with your Drupal site. Please note that it MUST be SSL if the hostname is not "localhost"'), + '#default_value' => variable_get('qbwc_url', str_replace('http:', 'https:', url('qbwc', null, null, true))), + ); - $form['qbwc']['qbwc_user'] = array( - '#type' => 'textfield', - '#title' => t('Quickbooks user'), - '#description' => t('This is the user that will be used to effect Quickbooks transactions. You can use an existing user, but you probably want to create a user for this purpose'), - '#autocomplete_path' => 'user/autocomplete', - '#default_value' => variable_get('qbwc_user', ''), - ); + $form['qbwc']['qbwc_user'] = array( + '#type' => 'textfield', + '#title' => t('Quickbooks user'), + '#description' => t('This is the user that will be used to effect Quickbooks transactions. You can use an existing user, but you probably want to create a user for this purpose'), + '#autocomplete_path' => 'user/autocomplete', + '#default_value' => variable_get('qbwc_user', ''), + ); - $form['qbwc']['qbwc_hostname'] = array( - '#type' => 'textfield', - '#title' => t('IP Restriction'), - '#description' => t('If you want to limit connections to the Web Connector service to a limited set of hosts, enter one or more IP addresses here, separated by commas.'), - ); + $form['qbwc']['qbwc_hostname'] = array( + '#type' => 'textfield', + '#title' => t('IP Restriction'), + '#description' => t('If you want to limit connections to the Web Connector service to a limited set of hosts, enter one or more IP addresses here, separated by commas.'), + ); - $options = array( - 0 => t('Manually (No scheduling)'), - 5 => t('Every 5 minutes'), - 15 => t('Every 15 minutes'), - 60 => t('Every hour'), - 240 => t('Every 4 hours'), - 480 => t('Every 8 hours'), - 480 => t('Every 8 hours'), - 720 => t('Every 12 hours'), - 1440 => t('Once per day'), - ); - $form['qbwc']['qbwc_scheduler'] = array( - '#type' => 'select', - '#title' => 'Run the Web Connector...', - '#description' => t('Select the time interval you wish to poll this site for updates.'), - '#default_value' => variable_get('qbwc_scheduler', 0), - '#options' => $options, - ); + $options = array( + 0 => t('Manually (No scheduling)'), + 5 => t('Every 5 minutes'), + 15 => t('Every 15 minutes'), + 60 => t('Every hour'), + 240 => t('Every 4 hours'), + 480 => t('Every 8 hours'), + 480 => t('Every 8 hours'), + 720 => t('Every 12 hours'), + 1440 => t('Once per day'), + ); + $form['qbwc']['qbwc_scheduler'] = array( + '#type' => 'select', + '#title' => 'Run the Web Connector...', + '#description' => t('Select the time interval you wish to poll this site for updates.'), + '#default_value' => variable_get('qbwc_scheduler', 0), + '#options' => $options, + ); - $form['qbwc']['qbwc_qwc'] = array( - '#type' => 'markup', - '#value' => l(t('Download QWC file'), 'admin/settings/qb/qwc'), - ); - } + $form['qbwc']['qbwc_qwc'] = array( + '#type' => 'markup', + '#value' => l(t('Download QWC file'), 'admin/settings/qb/qwc'), + ); + + return system_settings_form($form); } function qbwc_qwc() { -// TODO generate these! -variable_set('qbwc_file_id', '72832751-65dc-11db-bd13-0800200c9a66'); -variable_set('qbwc_owner_id', '72832751-65dc-11db-bd13-0800200c9a66'); $elements = array(); + + // Generate a FileID for this application file + $part = array(); + foreach(array(8,4,4,4,12) as $len) { + $part[] = substr(str_pad(base_convert(rand(), 10, 16), $len, "0", STR_PAD_LEFT), 0, $len); + } + $elements['AppName'] = variable_get('site_name', 'Drupal'); $elements['AppID'] = ''; $elements['AppURL'] = variable_get('qbwc_url', ''); @@ -91,27 +106,28 @@ variable_set('qbwc_owner_id', '72832751- $elements['AppSupport'] = variable_get('qbwc_url', '') .'/support'; $elements['UserName'] = variable_get('qbwc_user', ''); $elements['OwnerID'] = '{' . variable_get('qbwc_owner_id', '') . '}'; - $elements['FileID'] = '{' . variable_get('qbwc_file_id', '') . '}'; + $elements['FileID'] = '{' . implode('-', $part) . '}'; $elements['QBType'] = 'QBFS'; // Used with QB Financial systems, not POS. if ($minutes = variable_get('qbwc_scheduler', 0)) { $elements['Scheduler'] = array(); $elements['Scheduler']['RunEveryNMinutes'] = $minutes; } - $xml = qb_qbxml('QBWCXML', $elements); + $xml = qb_qbxml($elements, 'QBWCXML'); - header('Content-type: application/force-download'); - header('Content-Transfer-Encoding: Binary'); - header('Content-length: ' . strlen($xml)); - header('Content-disposition: attachment; filename=drupal.qwc'); + drupal_set_header('Content-type: application/force-download'); + drupal_set_header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + drupal_set_header('Content-Transfer-Encoding: Binary'); + drupal_set_header('Content-length: ' . strlen($xml)); + drupal_set_header('Content-disposition: attachment; filename=drupal.qwc'); echo $xml; + exit; } function qbwc() { - global $base_path; + global $base_path, $base_url; require_once dirname(__FILE__) . '/qbwc.inc'; - $wsdl = 'http://developer.intuit.com/uploadedFiles/Support/QBWebConnectorSvc.wsdl'; - $wsdl = 'https://dev/sites/qb/modules/qb/QBWebConnectorSvc.wsdl?'.time(); + $wsdl = $base_url .'/'. drupal_get_path('module', 'qbwc') .'/qbwc.wsdl'; $qbwc = new SoapServer($wsdl, array('uri' => variable_get('qbwc_url', ''))); $qbwc->setClass('qbwc'); $qbwc->setPersistence(SOAP_PERSISTENCE_SESSION); diff -Nup ./qbwc.wsdl ../../modules/qb/qbwc.wsdl --- ./qbwc.wsdl 1970-01-01 09:00:00.000000000 +0900 +++ ../../modules/qb/qbwc.wsdl 2008-07-23 12:11:49.000000000 +0900 @@ -0,0 +1,312 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file Common subdirectories: ./.svn and ../../modules/qb/.svn