diff --git a/api_objDef.php b/api_objDef.php index 7eeb7f0..6408aad 100644 --- a/api_objDef.php +++ b/api_objDef.php @@ -24,12 +24,21 @@ public function __construct(simpleXmlElement $simpleXml) { $this->Description = (string)$simpleXml->Attributes->Description; $fields = $simpleXml->Fields; - $this->Fields = array(); + foreach($fields->Field as $field) { $fieldDef = new api_fieldDef($field); $this->Fields[$fieldDef->Name] = $fieldDef; } } -} \ No newline at end of file + public function get_field_array() + { + $array = array(); + foreach ($this->Fields as $field) { + $array[] = (string)$field->Name; + } + return $array; + } + +} diff --git a/api_post.php b/api_post.php index 16a3c66..23c0b9b 100755 --- a/api_post.php +++ b/api_post.php @@ -1,4 +1,4 @@ -$object$id$fields$response_type"; + $response = api_post::post($readXml, $session); + api_post::validateReadResults($response); + switch ($response_type) { + case 'xml': + $resultRecallsArr = new SimpleXMLElement($response); + $result = $resultRecallsArr->operation->result->data; + break; + case 'csv': + $objAry = api_util::csvToPhp($response); + if (count(explode(",",$id)) > 1) { + $result = $objAry; + } + else { + $result = $objAry[0]; + } + break; + default: + $result = false; + break; + } + return $result; + } + + /** + * ReadDocument one or more records by their key. For platform objects, the key is the 'id' field. + * For standard objects, the key is the 'recordno' field. Results are returned as a php structured array * @param String $object the integration name for the object + * @param String $docparid the transaction type * @param String $id a comma separated list of keys for each record you wish to read * @param String $fields a comma separated list of fields to return * @param \api_session|Object $session an instance of the php_session object * @return Array of records */ - public static function read($object, $id, $fields, api_session $session) { + public static function readDocument($object, $docparid, $id, $fields, api_session $session) { - $readXml = "$object$id$fieldscsv"; + $readXml = "$object$id$fieldscsv$docparid"; $objCsv = api_post::post($readXml, $session); api_post::validateReadResults($objCsv); $objAry = api_util::csvToPhp($objCsv); @@ -49,7 +88,7 @@ public static function read($object, $id, $fields, api_session $session) { * @throws Exception * @return Array array of keys to the objects created */ - public static function create($records, api_session $session) { + public static function create($records, api_session $session,$policy=null) { if (count($records) > 100) throw new Exception("Attempting to create more than 100 records. (" . count($records) . ") "); @@ -63,9 +102,11 @@ public static function create($records, api_session $session) { $createXml = $createXml . $objXml; } $createXml = $createXml . ""; - $res = api_post::post($createXml, $session); + $res = api_post::post($createXml, $session, "3.0", false, $policy); + if ($policy !== null) { + return $res; + } $records = api_post::processUpdateResults($res, $node); - return $records; } @@ -82,7 +123,7 @@ public static function create($records, api_session $session) { * @return array An array of 'ids' updated in the method invocation */ public static function update($records, api_session $session) { - if (count($records) > 100) throw new Exception("Attempting to update more than 100 records."); + if (count($records) > 10000) throw new Exception("Attempting to update more than 10000 records."); // convert the $records array into an xml structure $updateXml = ""; @@ -167,7 +208,7 @@ public static function upsert($object, $records, $nameField, $keyField, api_sess unset($toCreate[$key][$object][$nameField]); } } - api_post::create($toCreate, $session); + return api_post::create($toCreate, $session); } if (count($toUpdate) > 0) { foreach ($toUpdate as $updateKey => $updateRec) { @@ -176,7 +217,7 @@ public static function upsert($object, $records, $nameField, $keyField, api_sess unset($toUpdate[$updateKey][$object][$nameField]); } } - api_post::update($toUpdate, $session); + return api_post::update($toUpdate, $session); } } } @@ -188,9 +229,9 @@ public static function upsert($object, $records, $nameField, $keyField, api_sess * objects and 'recordno' values for standard objects * @param api_session $session instance of api_session object */ - public static function delete($object, $ids, api_session $session) { + public static function delete($object, $ids, api_session $session,$policy=null) { $deleteXml = "$object$ids"; - api_post::post($deleteXml, $session); + api_post::post($deleteXml, $session,"3.0",false,$policy); } /** @@ -205,6 +246,15 @@ public static function otherMethod($xml, api_session $session, $dtdVersion="3.0" return api_post::post($xml, $session,$dtdVersion); } + public static function send_xml($xml, api_session $session) { + return api_post::post("$xml", $session,"3.0",true); + } + + public static function get_xml($obj) { + $xml = api_util::phpToXml('content',array($obj)); + return $xml; + } + /** * Run any Intacct API method not directly implemented in this class. You must pass * valid XML for the method you wish to invoke. @@ -221,16 +271,98 @@ public static function call21Method($function, $phpObj, api_session $session) { /** * Run any Intacct API method not directly implemented in this class. You must pass * valid XML for the method you wish to invoke. - * @param Array $phpObj an array for all the functions . + * @param Array $phpObj an array for all the functions . * @param api_session $session an api_session instance with a valid connection * @param string $dtdVersion DTD Version. Either "2.1" or "3.0". Defaults to "2.1" * @return String the XML response from Intacct */ - public static function sendFunctions($phpObj, api_session $session, $dtdVersion="2.1") { + public static function sendFunctions($phpObj, api_session $session, $dtdVersion="3.0", $returnFormat = api_returnFormat::XML,$policy = null) { $xml = api_util::phpToXml('content',array($phpObj)); - return api_post::post($xml, $session,$dtdVersion, true); + $res = api_post::post($xml, $session,$dtdVersion, true, $policy); + if ($returnFormat == api_returnFormat::PHPOBJ) { + $res_xml = simplexml_load_string($res); + $json = json_encode($res_xml->operation->result->data,JSON_FORCE_OBJECT); + $array = json_decode($json,TRUE); + return $array; + } else { + return $res; + } + } + + public static function prune_empty_element($a) { + foreach ($a as $k => $v) { + if (is_array($v) && empty($v)) { + $a[$k] = ""; + } + } + return $a; } + /** + * Run any Intacct API method not directly implemented in this class. You must pass + * valid XML for the method you wish to invoke. + * @param Array $phpObj an array for all the functions . + * @param api_session $session an api_session instance with a valid connection + * @param string $dtdVersion DTD Version. Either "2.1" or "3.0". Defaults to "2.1" + * @return String the XML response from Intacct + */ + public static function query(api_session $session, $call, int $limit = null, $returnFormat = api_returnFormat::PHPOBJ) { + $obj = $call['object']; + $call['offset'] = $call['offset'] ?? '0'; + $call['pagesize'] = $call['pagesize'] ?? '100'; + if ($limit !== null) { + $call['pagesize'] = min($limit,$call['pagesize']); + } + + $phpObj = array ( + 'function' => array ( + '@controlid' => uniqid(), + 'query' => $call, + ) + ); + $rows = array(); + dbg("**RBQ**: (QUERY) " . $obj ); + + do { + $xml = api_util::phpToXml('content',array($phpObj)); + $res = api_post::post($xml, $session,'3.0', true); + //dbg($res); + $res_xml = simplexml_load_string($res); + $json = json_encode($res_xml->operation->result->data,JSON_FORCE_OBJECT); + $array = json_decode($json,TRUE); + if (!isset($array[$obj])) { + $array[$obj] = array(); + } else if (!is_numeric(key($array[$obj]))) { + $array[$obj] = array( + $array[$obj] + ); + } + + $row = array_map(array('api_post','prune_empty_element'),$array[$obj]); + //dbg("READ this many rows" . count($row)); + $rows = array_merge($rows,$row); + $num_remaining = $array['@attributes']['numremaining']; + $total = count($rows); + if ($num_remaining > 0 && $limit !== null) { + $num_remaining = min($limit - $total,$num_remaining); + } + + $phpObj['function']['query']['offset'] += $phpObj['function']['query']['pagesize'] ; + dbg("REMAINING: $num_remaining. OFFSET is now : " . $phpObj['function']['query']['offset']); + + if ($limit !== null && $phpObj['function']['query']['offset'] >= $limit) { + $num_remaining = 0; + } + dbg(" **NUM REMAINING**: " . $num_remaining); + + } while ($num_remaining > 0); + + if ($returnFormat == api_returnFormat::PHPOBJ) { + return $rows; + } else { + die("only php return format supported"); + } + } /** * Run any Intacct API method not directly implemented in this class. You must pass * valid XML for the method you wish to invoke. @@ -240,9 +372,12 @@ public static function sendFunctions($phpObj, api_session $session, $dtdVersion= * @param string $dtdVersion DTD Version. Either "2.1" or "3.0". Defaults to "2.1" * @return String the XML response from Intacct */ - public static function get_list($object, $filter, $sorts, $fields, api_session $session, $dtdVersion="2.1") { + public static function get_list($object, $filter, $sorts, $fields, api_session $session, $dtdVersion="2.1", $max_desired = null) { $get_list = array(); $get_list['@object'] = $object; + $get_list['@start'] = 0; + $get_list['@maxitems'] = min(1000,$max_desired); + if ($filter != null) { $get_list['filter'] = $filter; } @@ -259,15 +394,67 @@ public static function get_list($object, $filter, $sorts, $fields, api_session $ ); $xml = api_util::phpToXml('content',array($func)); - $res = api_post::post($xml, $session,$dtdVersion, true); + $res = api_post::post($xml, $session,$dtdVersion, true); + if (self::$dryRun == true) { + return; + } $ret = api_post::processListResults($res, api_returnFormat::PHPOBJ, $count); - $toReturn = $ret[$object]; + $toReturn = null; + if (array_key_exists($object,$ret)) { + $toReturn = $ret[$object]; + } else { + return array(); + } if (is_array($toReturn)) { $keys = array_keys($toReturn); if (!is_numeric($keys[0])) { $toReturn = array ($toReturn); } } + + // now get more if there are any + $xml = simplexml_load_string($res); + $total = $xml->operation->result->listtype; + $attrs = $total->attributes(); + $total = $attrs['total']; + $c = count($toReturn); + + if ($c < $total && ($max_desired == null || $c < $max_desired )) { + + do { + dbg("FETCH MORE " . count($toReturn) . " of $total ($max_desired)"); + // we need to fetch more + $get_list['@start'] = count($toReturn); + $func['function'] = array(); + + $func['function'][] = array ( + '@controlid' => 'control1', + 'get_list' => $get_list + ); + + $xml = api_util::phpToXml('content',array($func)); + $res = api_post::post($xml, $session,$dtdVersion, true); + $ret = api_post::processListResults($res, api_returnFormat::PHPOBJ, $count); + + if (!is_array($ret) || empty($ret)) { + break; + } + + $nextBatch = null; + if (array_key_exists($object,$ret)) { + $nextBatch = $ret[$object]; + } + if (is_array($nextBatch)) { + $keys = array_keys($nextBatch); + if (!is_numeric($keys[0])) { + $nextBatch = array ($nextBatch); + } + } + $toReturn = array_merge($toReturn,$nextBatch); + + } while ( count($toReturn) < $total) ; + } + return $toReturn; } @@ -349,7 +536,7 @@ public static function readView($viewName, api_session $session, api_viewFilters // append all but the first row to the CSV file $page = explode("\n", $page); array_shift($page); - $csv .= implode($page, "\n"); + $csv .= implode("\n",$page); } elseif ($returnFormat == api_returnFormat::XML) { // just add the xml string @@ -377,6 +564,7 @@ public static function readView($viewName, api_session $session, api_viewFilters * @return mixed either string or array of objects depending on returnFormat argument */ public static function readByQuery($object, $query, $fields, api_session $session, $maxRecords=self::DEFAULT_MAXRETURN, $returnFormat=api_returnFormat::PHPOBJ) { + dbg("RBQ: $object -> $query with $fields"); $pageSize = ($maxRecords <= self::DEFAULT_PAGESIZE) ? $maxRecords : self::DEFAULT_PAGESIZE; @@ -393,6 +581,205 @@ public static function readByQuery($object, $query, $fields, api_session $sessio $readXml = "$object$query$fields$returnFormatArg"; $readXml .= "$pageSize"; $readXml .= ""; + //dbg($readXml); + + $response = api_post::post($readXml,$session); + if ($returnFormatArg == api_returnFormat::CSV && trim($response) == "") { + // csv with no records will have no response, so avoid the error from validate and just return + return ''; + } + if ($object == 'PROJECT') { + dbg(api_post::getLastRequest()); + dbg($response); + } + api_post::validateReadResults($response); + + + $phpobj = array(); $csv = ''; $json = ''; $xml = ''; $count = 0; $thiscount = 0; + $$returnFormat = self::processReadResults($response, $returnFormat, $thiscount); + + $totalcount = $thiscount; + //dbg("$thiscount == $pageSize && $totalcount <= $maxRecords"); + + // we have no idea if there are more if CSV is returned, so just check + // if the last count returned was $pageSize + while($thiscount == $pageSize && $totalcount < $maxRecords) { + $readXml = "$object"; + try { + $response = api_post::post($readXml, $session); + if ($object == 'PROJECT') { + dbg("READMORE PROJECT"); + dbg(api_post::getLastRequest()); + dbg($response); + } + api_post::validateReadResults($response); + $page = self::processReadResults($response, $returnFormat, $pageCount); + $totalcount += $pageCount; + $thiscount = $pageCount; + + switch($returnFormat) { + case api_returnFormat::PHPOBJ: + foreach($page as $objRec) { + $phpobj[] = $objRec; + } + break; + case api_returnFormat::CSV: + $page = explode("\n", $page); + array_shift($page); + $csv .= implode("\n",$page); + break; + case api_returnFormat::XML: + $xml .= $page; + break; + default: + throw new Exception("Invalid return format: " . $returnFormat); + break; + } + dbg("READMORE GOT: $thiscount, Total now: $totalcount"); + + } + catch (Exception $ex) { + // we've probably exceeded the limit + break; + } + } + return $$returnFormat; + } + /** + * Read records using a query. Specify the object you want to query and something like a "where" clause" + * @param api_session $session An instance of the api_session object with a valid connection + * @param String $object the object upon which to run the query + * @param String $fields A comma separated list of fields to return + * @param String $query the query string to execute. Use SQL operators + * @param int $maxRecords number of records to return. Defaults to 100000 + * @param string $returnFormat defaults to php object. Pass one of the valid constants from api_returnFormat class + * @return mixed either string or array of objects depending on returnFormat argument + */ + public static function query_bad(api_session $session, $object, $fields=null, $query=null, $maxRecords=self::DEFAULT_MAXRETURN, $returnFormat=api_returnFormat::PHPOBJ) { + + $pageSize = ($maxRecords <= self::DEFAULT_PAGESIZE) ? $maxRecords : self::DEFAULT_PAGESIZE; + + if ($returnFormat == api_returnFormat::PHPOBJ) { + $returnFormatArg = api_returnFormat::CSV; + } + else { + $returnFormatArg = $returnFormat; + } + + $field_xml = ""; + if ($fields !== null) { + $field_xml = "" . str_replace(",","",$fields) . ""; + } + if ($query !== NULL) { + $query_xml = api_util::phpToXml($query); + + } + + // TODO: Implement returnFormat. Today we only support PHPOBJ +// $query = HTMLSpecialChars($query); + + $readXml = "$object$query_xml"; + $readXml .= "$pageSize"; + $readXml .= ""; + //dbg($readXml); + + $response = api_post::post($readXml,$session); + //dbg($response); + die(); + if ($returnFormatArg == api_returnFormat::CSV && trim($response) == "") { + // csv with no records will have no response, so avoid the error from validate and just return + return ''; + } + api_post::validateReadResults($response); + + + $phpobj = array(); $xml = ''; $count = 0; $thiscount = 0; + + $simpleXml = simplexml_load_string($response); + $data = $simpleXml->xpath("/response/operation/result/data"); + $thiscount = $data[0]['count']; + $rows = $simpleXml->xpath("/response/operation/result/data/$object"); + foreach ($rows as $row) { + $phpobj[] = (array)$row; + } + + if ($data[0]['numremaining']) + +// $thiscount = $data-> + + //$$returnFormat = self::processReadResults($response, $returnFormat, $thiscount); + + $totalcount = $thiscount; + //dbg("$thiscount == $pageSize && $totalcount <= $maxRecords"); + + // we have no idea if there are more if CSV is returned, so just check + // if the last count returned was $pageSize + while($thiscount == $pageSize && $totalcount < $maxRecords) { + $readXml = "$object"; + try { + $response = api_post::post($readXml, $session); + //dbg($response); + //dbg(api_post::getLastRequest()); + api_post::validateReadResults($response); + $page = self::processReadResults($response, $returnFormat, $pageCount); + $totalcount += $pageCount; + $thiscount = $pageCount; + + switch($returnFormat) { + case api_returnFormat::PHPOBJ: + foreach($page as $objRec) { + $phpobj[] = $objRec; + } + break; + case api_returnFormat::CSV: + $page = explode("\n", $page); + array_shift($page); + $csv .= implode($page, "\n"); + break; + case api_returnFormat::XML: + $xml .= $page; + break; + default: + throw new Exception("Invalid return format: " . $returnFormat); + break; + } + + } + catch (Exception $ex) { + // we've probably exceeded the limit + break; + } + } + return $$returnFormat; + } + /** + * Read records using a query. Specify the object you want to query and something like a "where" clause" + * @param String $object the object upon which to run the query + * @param String $query the query string to execute. Use SQL operators + * @param String $fields A comma separated list of fields to return + * @param api_session $session An instance of the api_session object with a valid connection + * @param int $maxRecords number of records to return. Defaults to 100000 + * @param string $returnFormat defaults to php object. Pass one of the valid constants from api_returnFormat class + * @return mixed either string or array of objects depending on returnFormat argument + */ + public static function readDocumentByQuery($object, $docparid, $query, $fields, api_session $session, $maxRecords=self::DEFAULT_MAXRETURN, $returnFormat=api_returnFormat::PHPOBJ) { + + $pageSize = ($maxRecords <= self::DEFAULT_PAGESIZE) ? $maxRecords : self::DEFAULT_PAGESIZE; + + if ($returnFormat == api_returnFormat::PHPOBJ) { + $returnFormatArg = api_returnFormat::CSV; + } + else { + $returnFormatArg = $returnFormat; + } + + // TODO: Implement returnFormat. Today we only support PHPOBJ + $query = HTMLSpecialChars($query); + + $readXml = "$object$query$fields$returnFormatArg"; + $readXml .= "$docparid"; + $readXml .= "$pageSize"; + $readXml .= ""; $response = api_post::post($readXml,$session); if ($returnFormatArg == api_returnFormat::CSV && trim($response) == "") { @@ -409,7 +796,7 @@ public static function readByQuery($object, $query, $fields, api_session $sessio // we have no idea if there are more if CSV is returned, so just check // if the last count returned was $pageSize - while($thiscount == $pageSize && $totalcount <= $maxRecords) { + while($thiscount == $pageSize && $totalcount < $maxRecords) { $readXml = "$object"; try { $response = api_post::post($readXml, $session); @@ -446,6 +833,7 @@ public static function readByQuery($object, $query, $fields, api_session $sessio return $$returnFormat; } + /** * Inspect an object to get a list of its fields * @@ -504,8 +892,90 @@ public static function readByName($object, $name, $fields, api_session $session) public static function readRelated($object, $keys, $relation, $fields, api_session $session) { $readXml = "$object$keys$relation$fieldscsv"; $objCsv = api_post::post($readXml, $session); + //if we receive an empty response we return it + if (trim($objCsv) == "") { + return ''; + } + api_post::validateReadResults($objCsv); + $objAry = api_util::csvToPhp($objCsv); + return $objAry; + } + + /** + * Reads all the records related to a source record through a named relationship. + * @param String $object the integration name of the object + * @param String $keys a comma separated list of 'id' values of the source records from which you want to read related records + * @param String $relation the name of the relationship. This will determine the type of object you are reading + * @param String $fields a comma separated list of fields to return + * @param api_session $session + * @return Array of objects + */ + public static function readReport($report, api_session $session, $arguments=null, $waitTime=0, $pageSize=100) { + $maxRecords = self::DEFAULT_MAXRETURN; + $max_try = 1000; + $try = 0; + + if (is_array($arguments) ) { + $argxml= ""; + foreach ($arguments as $key => $arg) { + $argxml.= "<$key>$arg"; + } + $argxml .= ""; + } + $readXml = "$reportcsv$waitTime$argxml$pagesize"; + $objCsv = api_post::post($readXml, $session); api_post::validateReadResults($objCsv); $objAry = api_util::csvToPhp($objCsv); + + if (is_array($objAry) && count($objAry) == 1) { + $id = $objAry[0]['REPORTID']; + do { + $readXml = "$id"; + try { + $response = api_post::post($readXml, $session); + //dbg("READMORE:"); + //dbg($response); + //dbg("TRIMMED:"); + //dbg(trim($response)); + if (trim($response) == "") { + return array(); + } + api_post::validateReadResults($response); + $_obj = api_util::csvToPhp($response); + if (isset($_obj[0]['STATUS']) && $_obj[0]['STATUS'] == 'PENDING') { + //dbg("Sleeping 10, try = $try"); + sleep("10"); + $try++; + continue; + } + + $page = self::processReadResults($response, $returnFormat, $pageCount); + $count += $pageCount; + if ($returnFormat == api_returnFormat::PHPOBJ) { + foreach($page as $objRec) { + $phpobj[] = $objRec; + } + } + elseif ($returnFormat == api_returnFormat::CSV) { + // append all but the first row to the CSV file + $page = explode("\n", $page); + array_shift($page); + $csv .= implode($page, "\n"); + } + elseif ($returnFormat == api_returnFormat::XML) { + // just add the xml string + $xml .= $page; + } + } + catch (Exception $ex) { + // for now, pass the exception on + Throw new Exception($ex); + } + if ($pageCount < $pageSize || $count >= $maxRecords) break; + } while ($try < $max_try); + + //dbg("FINISHED LOOP"); + } return $objAry; } @@ -554,22 +1024,39 @@ public static function deleteAll($object, api_session $session, $max=10000) { * @param Integer $max [optional] Maximum number of records to delete. Default is 10000 * @return Integer count of records deleted */ - public static function deleteByQuery($object, $query, api_session $session, $max=10000) { + public static function deleteByQuery($object, $query, $key_field, api_session $session, $max=100000) { + $num_per_func= 100; // read all the record ids for the given object - $ids = api_post::readByQuery($object, "id > 0 and $query", "id", $session, $max); + $ids = api_post::readByQuery($object, "$key_field > 0 and $query", $key_field, $session, $max); + if (!is_array($ids)) { + $ids = array(); + } if ((!is_array($ids) && trim($ids) == '') || !count($ids) > 0) { return 0; } + dbg("COUNT of things to delete: " . count($ids)); + $count = 0; $delIds = array(); + $_count = count($ids); + foreach($ids as $rec) { - $delIds[] = $rec['id']; - if (count($delIds) == 100) { - api_post::delete($object, implode(",", $delIds), $session); - $count += 100; + $delIds[] = $rec[$key_field]; + if (count($delIds) == $num_per_func) { + try { + $_count -= $num_per_func; + dbg($_count); + api_post::delete($object, implode(",", $delIds), $session); + } + catch (Exception $ex) { + $delIds = array(); + print_r($ex); + continue; + } + $count += $num_per_func; $delIds = array(); } } @@ -590,7 +1077,7 @@ public static function deleteByQuery($object, $query, api_session $session, $max * @throws Exception * @return String the XML response document */ - private static function post($xml, api_session $session, $dtdVersion="3.0",$multiFunc=false) { + private static function post($xml, api_session $session, $dtdVersion="3.0",$multiFunc=false, $policy=null) { $sessionId = $session->sessionId; $endPoint = $session->endPoint; @@ -599,7 +1086,7 @@ private static function post($xml, api_session $session, $dtdVersion="3.0",$mult $transaction = ( $session->transaction ) ? 'true' : 'false' ; - /* + /* $templateHead = " @@ -632,16 +1119,26 @@ private static function post($xml, api_session $session, $dtdVersion="3.0",$mult */ - $templateHead = -" + + // if self::$sage_appid is not empty we want to pass above senderid. + + $templateHead = " - + "; +if (!empty(self::$sage_appid)) { + $templateHead .= " + " . self::$sage_appid . ""; +} +$templateHead .= " {$senderId} {$senderPassword} - foobar + ".uniqid()." false - {$dtdVersion} - + {$dtdVersion}"; + if ($policy !== null) { + $templateHead .= "$policy"; + } + $templateHead .= " {$sessionId} @@ -651,7 +1148,7 @@ private static function post($xml, api_session $session, $dtdVersion="3.0",$mult " "; - $contentFoot = + $contentFoot = " "; @@ -669,6 +1166,7 @@ private static function post($xml, api_session $session, $dtdVersion="3.0",$mult if (self::$dryRun == true) { self::$lastRequest = $xml; + self::$lastResponse= null; return; } @@ -677,20 +1175,40 @@ private static function post($xml, api_session $session, $dtdVersion="3.0",$mult $res = ""; while (true) { $res = api_post::execute($xml, $endPoint); + if ( strpos($res, "520 Origin") !== FALSE || strpos($res, "502 Bad Gateway") !== FALSE) { + $count++; + if ($count <= 5) { + dbg("RETRYING due to 520 or 502 error"); + dbg("===REQUEST=============================="); + dbg(api_post::getLastRequest()); + dbg("===RESPONSE=============================="); + dbg($res); + continue; + } + } // If we didn't get a response, we had a poorly constructed XML request. try { api_post::validateResponse($res, $xml); break; - } - catch (Exception $ex) { + } catch (Exception $ex) { + dbg("JPC EXCEPTION"); + if ($count >= 5) { + throw new Exception($ex->getMessage(),$ex->getCode(),$ex); + } + dbg("EX getMessage"); + dbg($ex->getMessage()); if (strpos($ex->getMessage(), "too many operations") !== false) { $count++; - if ($count >= 5) { - throw new Exception($ex); - } + } else if (strpos($ex->getMessage(), "UJPP0007") !== false) { + $count++; + dbg("Got UJPP007. Sleeping one minute and trying again."); + dbg("RESPONSE HEADER:"); + dbg(self::$lastResponseHeader); + dbg("END RESPONSE HEADER:"); + sleep(60); } else { - throw new Exception($ex); + throw new Exception($ex->getMessage(),$ex->getCode(),$ex); } } } @@ -708,29 +1226,55 @@ private static function post($xml, api_session $session, $dtdVersion="3.0",$mult public static function execute($body, $endPoint) { self::$lastRequest = $body; + self::$lastResponse = null; $ch = curl_init(); curl_setopt( $ch, CURLOPT_URL, $endPoint ); - curl_setopt( $ch, CURLOPT_HEADER, 0 ); + curl_setopt( $ch, CURLOPT_HEADER, true ); + curl_setopt( $ch, CURLINFO_HEADER_OUT, true ); curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 ); curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 ); - curl_setopt( $ch, CURLOPT_TIMEOUT, 3000 ); //Seconds until timeout + curl_setopt( $ch, CURLOPT_TIMEOUT, 6000 ); //Seconds until timeout curl_setopt( $ch, CURLOPT_POST, 1 ); + curl_setopt( $ch, CURLOPT_VERBOSE, 0); // TODO: Research and correct the problem with CURLOPT_SSL_VERIFYPEER curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false); // yahoo doesn't like the api.intacct.com CA $body = "xmlrequest=" . urlencode( $body ); - curl_setopt( $ch, CURLOPT_POSTFIELDS, $body ); + curl_setopt( $ch, CURLOPT_POSTFIELDS, $body ); $response = curl_exec( $ch ); $error = curl_error($ch); + $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); + + $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); + $response_header = substr($response, 0, $header_size); + $response_body = substr($response, $header_size); + self::$lastResponse = $response_body; + self::$lastResponseHeader = $response_header; + + if ($code != "200" || strpos($response,"UJPP00") !== FALSE || + strpos($error,"transfer closed") !== FALSE || + strpos($response,"cloudflare-nginx") !== FALSE || + strpos($error,"cloudflare-nginx") !== FALSE || + strpos($response,"") !== FALSE) { + + dbg("SAGE ERROR FULL RESPONSE with header"); + dbg($response); + dbg("RESPONSE BODY"); + dbg($response_body); + dbg("SAGE ERROR REQUEST WAS"); + dbg("SAGE ERROR " . self::$lastRequest); + } + if ($error != "") { + dbg("FULL RESPONSE with header"); + dbg($response); throw new exception($error); } curl_close( $ch ); - self::$lastResponse = $response; - return $response; + return $response_body; } @@ -739,7 +1283,7 @@ public static function execute($body, $endPoint) { * @param String $response The XML response document * @throws Exception */ - public static function findResponseErrors($response) { + public static function findResponseErrors($response,$multi=false) { $errorArray = array(); @@ -756,7 +1300,7 @@ public static function findResponseErrors($response) { // look for a failure in the operation, but not the result if (isset($simpleXml->operation->errormessage)) { $error = $simpleXml->operation->errormessage->error[0]; - $errorArray[] = array ( 'desc' => api_util::xmlErrorToString($simpleXml->operation->errormessage)); + $errorArray[] = array ( 'desc' => api_util::xmlErrorToString($simpleXml->operation->errormessage,$multi)); } // if we didn't get an operation, the request failed and we should raise an exception @@ -764,14 +1308,14 @@ public static function findResponseErrors($response) { // did the method invocation fail? if (!isset($simpleXml->operation)) { if (isset($simpleXml->errormessage)) { - $errorArray[] = array ( 'desc' => api_util::xmlErrorToString($simpleXml->errormessage)); + $errorArray[] = array ( 'desc' => api_util::xmlErrorToString($simpleXml->errormessage,$multi)); } } else { $results = $simpleXml->xpath('/response/operation/result'); foreach ($results as $result) { - if ((string)$result->status == "failure") { - $errorArray[] = array ( 'controlid' => (string)$result->controlid, 'desc' => api_util::xmlErrorToString($result->errormessage)); + if ((string)$result->status == "failure" || (string)$result->status == "aborted") { + $errorArray[] = array ( 'controlid' => (string)$result->controlid, 'desc' => api_util::xmlErrorToString($result->errormessage,$multi)); } } } @@ -808,11 +1352,12 @@ private static function validateResponse($response) { throw new Exception("[Error] " . api_util::xmlErrorToString($simpleXml->errormessage)); } } - else { + else { $results = $simpleXml->operation->result; foreach ($results as $res) { - if ($res->status == "failure") { - throw new Exception("[Error] " . api_util::xmlErrorToString($res->errormessage)); + if ($res->status == "failure" || $res->status == "aborted") { + $msg = api_util::xmlErrorToString($res->errormessage); + throw new Exception("[Error] " . $msg); } } } @@ -827,6 +1372,8 @@ private static function validateResponse($response) { * @throws Exception */ private static function processUpdateResults($response, $objectName) { + //Fix Intacct bug, by trim spaces from the returned xml response string + $response = trim($response); $simpleXml = simplexml_load_string($response); if ($simpleXml === false) { throw new Exception("Invalid XML response: \n " . var_export($response, true)); @@ -900,7 +1447,8 @@ private static function validateReadResults($response) { * @throws Exception * @return Mixed string or object depending on return format */ - public static function processListResults($response, $returnFormat = api_returnFormat::PHPOBJ, &$count) { + public static function processListResults($response, $returnFormat, &$count) { + //dbg($response); $xml = simplexml_load_string($response); @@ -909,14 +1457,59 @@ public static function processListResults($response, $returnFormat = api_returnF throw new Exception("Get List failed"); return; } - + if ($returnFormat != api_returnFormat::PHPOBJ) { throw new Exception("Only PHPOBJ is supported for returnFormat currently."); return; } - $json = json_encode($xml->operation->result->data); + $json = json_encode($xml->operation->result->data,JSON_FORCE_OBJECT); + if ($json == "{}") { + return array(); + } + $array = json_decode($json,TRUE); + + $obj = key($array); + + if (!is_numeric(key($array[$obj]))) { + $array[$obj] = array ( $array[$obj] ); + } + + // check for known line item issues + // lame, but not sure how else to fix this. the json_decode removes the level from a single line item and it needs to be restored + // make this generic if it works + if (isset($array['sotransaction'])) { + foreach ($array['sotransaction'] as $key => $txn) { + if (isset($txn['sotransitems']['sotransitem'])) { + if (!is_numeric(key($txn['sotransitems']['sotransitem']))) { + $array['sotransaction'][$key]['sotransitems']['sotransitem'] = array ($txn['sotransitems']['sotransitem']); + } + } + } + } + if (isset($array['dimensions'])) { + $array['dimensions'] = $array['dimensions'][0]['dimension']; + } + if (isset($array['arpayment'])) { + foreach ($array['arpayment'] as $key => $txn) { + if (isset($txn['lineitems']['lineitem'])) { + if (!is_numeric(key($txn['lineitems']['lineitem']))) { + $array['arpayment'][$key]['lineitems']['lineitem'] = array ($txn['lineitems']['lineitem']); + } + } + } + } + + if (isset($array['recursotransaction'])) { + foreach ($array['recursotransaction'] as $key => $txn) { + if (isset($txn['recursotransitems']['recursotransitem'])) { + if (!is_numeric(key($txn['recursotransitems']['recursotransitem']))) { + $array['recursotransaction'][$key]['recursotransitems']['recursotransitem'] = array ($txn['recursotransitems']['recursotransitem']); + } + } + } + } return $array; } @@ -928,7 +1521,7 @@ public static function processListResults($response, $returnFormat = api_returnF * @throws Exception * @return Mixed string or object depending on return format */ - private static function processReadResults($response, $returnFormat = api_returnFormat::PHPOBJ, &$count) { + private static function processReadResults($response, $returnFormat, &$count) { $objAry = array(); $csv = ''; $json = ''; $xml = ''; if ($returnFormat == api_returnFormat::PHPOBJ) { $objAry = api_util::csvToPhp($response); @@ -980,9 +1573,18 @@ public static function getLastResponse() { return self::$lastResponse; } + public static function getLastResponseHeader() { + return self::$lastResponseHeader; + } + public static function setDryRun($tf=true) { self::$dryRun = $tf; } - + + public static function setAppID($id) + { + self::$sage_appid = $id; + } + } diff --git a/api_returnFormat.php b/api_returnFormat.php index 1f2fbb0..6272530 100755 --- a/api_returnFormat.php +++ b/api_returnFormat.php @@ -1,4 +1,4 @@ - "; + const XML_FOOTER_2 = " + + {6%} + + +"; const XML_LOGIN = " {1%} @@ -40,7 +47,22 @@ class api_session { const XML_SESSIONID = "{1%}"; const DEFAULT_LOGIN_URL = "https://api.intacct.com/ia/xml/xmlgw.phtml"; - + const PRV_LOGIN_URL = "https://preview.intacct.com/ia/xml/xmlgw.phtml"; + + public function __toString() { + $temp = array ( + 'sessionId' => $this->sessionId, + 'endPoint' => $this->endPoint, + 'companyId' => $this->companyId, + 'entityId' => $this->entityId, + 'userId' => $this->userId, + 'senderId' => $this->senderId, + 'entityId' => $this->entityId, + 'senderPassword' => 'REDACTED', + 'transaction' => $this->transaction + ); + return json_encode($temp); + } /** * Connect to the Intacct Web Service using a set of user credntials for a subentity @@ -53,7 +75,7 @@ class api_session { * @param String $entityId The sub entity id * @throws Exception this method returns no value, but will raise any connection exceptions */ - private function buildHeaderXML($companyId, $userId, $password, $senderId, $senderPassword, $entityType = null, $entityId = null ) + private function buildHeaderXML($companyId, $userId, $password, $senderId, $senderPassword, $entityType = null, $entityId = null ) { $xml = self::XML_HEADER . self::XML_LOGIN . self::XML_FOOTER; @@ -62,15 +84,16 @@ private function buildHeaderXML($companyId, $userId, $password, $senderId, $send $xml = str_replace("{2%}", $companyId, $xml); $xml = str_replace("{3%}", $password, $xml); $xml = str_replace("{4%}", $senderId, $xml); - $xml = str_replace("{5%}", $senderPassword, $xml); + $xml = str_replace("{5%}", htmlspecialchars($senderPassword), $xml); - if ($entityType == 'location') { + // hack for backward compat + if ($entityType == 'location' || $entityType === null) { $xml = str_replace("{%entityid%}", "$entityId", $xml); - } - else if ($entityType == 'client') { + } else if ($entityType == 'client') { $xml = str_replace("{%entityid%}", "$entityId", $xml); - } - else { + } else if (!empty($entityType) || !empty($entityId)) { + $xml = str_replace("{%entityid%}", "$entityType$entityId", $xml); + } else { $xml = str_replace("{%entityid%}", "", $xml); } @@ -88,9 +111,10 @@ private function buildHeaderXML($companyId, $userId, $password, $senderId, $send */ public function connectCredentials($companyId, $userId, $password, $senderId, $senderPassword, $entityType=null, $entityId=null) { - $xml = $this->buildHeaderXML($companyId, $userId, $password, $senderId, $senderPassword, $entityType, $entityId); + $xml = $this->buildHeaderXML($companyId, $userId, $password, $senderId, $senderPassword, $entityType, $entityId); - $response = api_post::execute($xml, self::DEFAULT_LOGIN_URL); + $endpoint = strpos($companyId,"-prv") === FALSE ? self::DEFAULT_LOGIN_URL : self::PRV_LOGIN_URL; + $response = api_post::execute($xml, $endpoint); self::validateConnection($response); @@ -98,6 +122,7 @@ public function connectCredentials($companyId, $userId, $password, $senderId, $s $this->sessionId = (string)$responseObj->operation->result->data->api->sessionid; $this->endPoint = (string)$responseObj->operation->result->data->api->endpoint; + $this->entityId = (string)$responseObj->operation->authentication->locationid; $this->companyId = $companyId; $this->userId = $userId; $this->senderId = $senderId; @@ -118,9 +143,10 @@ public function connectCredentials($companyId, $userId, $password, $senderId, $s */ public function connectCredentialsEntity($companyId, $userId, $password, $senderId, $senderPassword,$entityType, $entityId) { - $xml = $this->buildHeaderXML($companyId, $userId, $password, $senderId, $senderPassword,$entityType, $entityId); + $xml = $this->buildHeaderXML($companyId, $userId, $password, $senderId, $senderPassword,$entityType, $entityId); - $response = api_post::execute($xml, self::DEFAULT_LOGIN_URL); + $endpoint = strpos($companyId,"-prv") === FALSE ? self::DEFAULT_LOGIN_URL : self::PRV_LOGIN_URL; + $response = api_post::execute($xml, $endpoint); self::validateConnection($response); @@ -128,6 +154,7 @@ public function connectCredentialsEntity($companyId, $userId, $password, $sender $this->sessionId = (string)$responseObj->operation->result->data->api->sessionid; $this->endPoint = (string)$responseObj->operation->result->data->api->endpoint; + $this->entityId = (string)$responseObj->operation->authentication->locationid; $this->companyId = $companyId; $this->userId = $userId; $this->senderId = $senderId; @@ -143,14 +170,25 @@ public function connectCredentialsEntity($companyId, $userId, $password, $sender * @param String $senderPassword Your Intacct partner password * @throws Exception This method returns no values, but will raise an exception if there's a connection error */ - public function connectSessionId($sessionId, $senderId, $senderPassword) { - - $xml = self::XML_HEADER . self::XML_SESSIONID . self::XML_FOOTER; + public function connectSessionId($sessionId, $senderId, $senderPassword, $entityId = null) { + + if ($entityId === null) { + // we are passing NO entity/location. do not add locationid to the XML + $xml = self::XML_HEADER . self::XML_SESSIONID . self::XML_FOOTER; + } else { + // we are passing entity/location ('' for top-level flip from entity). add locationid to the XML + $xml = self::XML_HEADER . self::XML_SESSIONID . self::XML_FOOTER_2; + } + $xml = str_replace("{1%}", $sessionId, $xml); $xml = str_replace("{4%}", $senderId, $xml); $xml = str_replace("{5%}", $senderPassword, $xml); + if ($entityId !== null) { + $xml = str_replace("{6%}", $entityId, $xml); + } - $response = api_post::execute($xml, self::DEFAULT_LOGIN_URL); + $endpoint = ($this->companyId === null || strpos($this->companyId,"-prv") === FALSE) ? self::DEFAULT_LOGIN_URL : self::PRV_LOGIN_URL; + $response = api_post::execute($xml, $endpoint); self::validateConnection($response); @@ -159,6 +197,7 @@ public function connectSessionId($sessionId, $senderId, $senderPassword) { $this->companyId = (string)$responseObj->operation->authentication->companyid; $this->userId = (string)$responseObj->operation->authentication->userid; $this->endPoint = (string)$responseObj->operation->result->data->api->endpoint; + $this->entityId = (string)$responseObj->operation->authentication->locationid; $this->senderId = $senderId; $this->senderPassword = $senderPassword; } @@ -181,8 +220,10 @@ private static function validateConnection($response) { if (isset($simpleXml->operation->authentication->status)) { if ($simpleXml->operation->authentication->status != 'success') { + print_r($simpleXml); $error = $simpleXml->operation->errormessage; - throw new Exception(" [Error] " . (string)$error->error[0]->description2); + $desc2 = (string)$error->error[0]->description2 ?? ''; + throw new Exception(" [Error] " . $desc2); } } diff --git a/api_util.php b/api_util.php index 97b898b..c133ab5 100755 --- a/api_util.php +++ b/api_util.php @@ -1,4 +1,4 @@ -modify("-1 day"); $dates = array($dateTime->format("Y-m-d")); - // now, iterate $count - 1 times adding one month to each + // now, iterate $count - 1 times adding one month to each for ($x=1; $x < $count; $x++) { $dateTime->modify("+1 day"); $dateTime->modify("+1 month"); @@ -91,11 +91,11 @@ public static function getRangeOfDates($date, $count) { return $dates; } - /** - * Convert a php structure to an XML element + /** + * Convert a php structure to an XML element * @param String $key element name * @param Array $values element values - * @return string xml + * @return string xml */ public static function phpToXml($key, $values) { $xml = ""; @@ -103,13 +103,15 @@ public static function phpToXml($key, $values) { return "<$key>$values"; } - if (!is_numeric(array_shift(array_keys($values)))) { + $temp1 = array_keys($values); + $temp2 = array_shift($temp1); + if (!is_numeric($temp2)) { $xml = "<" . $key . ">"; } foreach($values as $node => $value) { $attrString = ""; $_xml = ""; - if (is_array($value)) { + if (is_array($value) && count($value) > 0) { if (is_numeric($node)) { $node = $key; } @@ -119,7 +121,7 @@ public static function phpToXml($key, $values) { if (substr($_k,0,1) == '@') { $pad = ($attrString == "") ? " " : ""; $aname = substr($_k,1); - $aval = $v; + $aval = htmlspecialchars($v); //$attrs = explode(':', substr($v,1)); //$attrString .= $pad . $attrs[0].'="'.$attrs[1].'" '; $attrString .= $pad . $aname.'="'.$aval.'" '; @@ -128,13 +130,17 @@ public static function phpToXml($key, $values) { } } - $firstKey = array_shift(array_keys($value)); - if (is_array($value[$firstKey]) || count($value) > 1 ) { - $_xml = self::phpToXml($node,$value) ; + $valuekeys = array_keys($value); + $firstKey = array_shift($valuekeys); + if (is_array($value) && (( isset($value[$firstKey]) && is_array($value[$firstKey])) || count($value) > 0)) { + $_xml = self::phpToXml($node,$value) ; } else { - $v = $value[$firstKey]; - $_xml .= "<$node>" . htmlspecialchars($v) . ""; + $_xml = self::phpToXml($node,$value) ; + $v = $value[$firstKey] ?? ''; + if (!empty($v)) { + $_xml .= "<$node>" . htmlspecialchars($v) . ""; + } } if ($attrString != "") { @@ -142,36 +148,48 @@ public static function phpToXml($key, $values) { } $xml .= $_xml; +// dbg("XML now is $xml"); } else { - $xml .= "<" . $node . $attrString . ">" . htmlspecialchars($value) . ""; + if (is_numeric($node)) { + $xml .= "<" . $key. $attrString . ">" . htmlspecialchars($value) . ""; + } + else { + $_v = ($value != "0" && empty($value)) ? '' : htmlspecialchars($value); + $xml .= "<" . $node . $attrString . ">" .$_v . ""; + } } } - if (!is_numeric(array_shift(array_keys($values)))) { + $temp1 = array_keys($values); + $temp2 = array_shift($temp1); + if (!is_numeric($temp2)) { $xml .= ""; } return $xml; } - /** - * Convert a CSV string result into a php array. - * This work for Intacct API results. Not a generic method + /** + * Convert a CSV string result into a php array. + * This work for Intacct API results. Not a generic method */ public static function csvToPhp($csv) { $fp = fopen('php://temp', 'r+'); + $csv = str_replace("\,",",",$csv); + $csv = str_replace('\",','",',$csv); fwrite($fp, trim($csv)); + rewind($fp); $table = array(); - // get the header row + // get the header row $header = fgetcsv($fp, 10000, ',','"'); if (is_null($header) || is_null($header[0])) { - throw new exception ("Unable to determine header. Is there garbage in the file?"); + throw new \Exception ("Unable to determine header. Is there garbage in the file?"); } - // get the rows + // get the rows while (($data = fgetcsv($fp, 10000, ',','"')) !== false) { $row = array(); foreach($header as $key => $value) { @@ -181,30 +199,40 @@ public static function csvToPhp($csv) { } return $table; - } - + } + /** * Convert a error object into nice text * @param Object $error simpleXmlObject * @return string formatted error message */ - public static function xmlErrorToString($error) { - + public static function xmlErrorToString($error,$multi=false) { if (!is_object($error)) { return "Malformed error: " . var_export($error, true); } - - $error = $error->error[0]; - if (!is_object($error)) { + // show just the first error + //$error = $error->error[0]; + $error_string = ""; + if (!isset($error->error)) { return "Malformed error: " . var_export($error, true); } + foreach ($error->error as $error) { + if (!is_object($error)) { + return "Malformed error: " . var_export($error, true); + } - $errorno = is_object($error->errorno) ? (string)$error->errorno : ' '; - $description = is_object($error->description) ? (string)$error->description : ' '; - $description2 = is_object($error->description2) ? (string)$error->description2 : ' '; - $correction = is_object($error->correction) ? (string)$error->correction : ' '; - return "$errorno: $description: $description2: $correction"; + $errorno = is_object($error->errorno) ? (string)$error->errorno : ' '; + $description = is_object($error->description) ? (string)$error->description : ' '; + $description2 = is_object($error->description2) ? (string)$error->description2 : ' '; + $correction = is_object($error->correction) ? (string)$error->correction : ' '; + $error_string .= "$errorno: $description: $description2: $correction\n"; + if ($multi === false) { + break; + } + } + return $error_string; } + } diff --git a/api_viewFilter.php b/api_viewFilter.php index 585d963..93f083c 100755 --- a/api_viewFilter.php +++ b/api_viewFilter.php @@ -1,4 +1,4 @@ -operator = $operator; $this->value = HTMLSpecialChars($value); } -} \ No newline at end of file +} diff --git a/api_viewFilters.php b/api_viewFilters.php index df66178..3950e3c 100755 --- a/api_viewFilters.php +++ b/api_viewFilters.php @@ -1,4 +1,4 @@ -filters = $filters; $this->operator = $operator; } -} \ No newline at end of file +}