diff --git a/QuickBooks/Callbacks/SQL/Callbacks.php b/QuickBooks/Callbacks/SQL/Callbacks.php index aa0e526a..14a5c944 100644 --- a/QuickBooks/Callbacks/SQL/Callbacks.php +++ b/QuickBooks/Callbacks/SQL/Callbacks.php @@ -10662,6 +10662,9 @@ protected static function _transformToSQLObjects($curpath, $Node, &$objects, $cu $extra['EntityListID'] = $extra['ListID']; $extra['EntityType'] = 'Employee'; break; + case 'employeeret employeepayrollinfo earnings': + $extra['EntityType'] = 'Earnings'; + break; case 'estimateret': if (!isset($extra['TxnID'])) { @@ -10984,6 +10987,8 @@ protected static function _transformToSQLObjects($curpath, $Node, &$objects, $cu case 'Service': case 'SubscribedServices': case 'TaxLineInfoRet': + case 'EmployeePayrollInfo': + case 'Earnings': // * * * WARNING WARNING WARNING * * * // The next line of code causes problems with some responses diff --git a/QuickBooks/Driver.php b/QuickBooks/Driver.php index 3c60c24b..0154722c 100755 --- a/QuickBooks/Driver.php +++ b/QuickBooks/Driver.php @@ -1435,8 +1435,64 @@ public function oauthRequestResolve($token) } abstract protected function _oauthRequestResolve($token); - - /** + + /** + * OAuth2 functions + */ + + public function oauth2Load($key, $app_username, $app_tenant) + { + if ($data = $this->_oauth2Load($app_username, $app_tenant)) + { + if (!empty($data['oauth2_access_token'])) + { + if (strlen($key) > 0) + { + $AES = QuickBooks_Encryption_Factory::create('aes'); + + $data['oauth2_access_token'] = $AES->decrypt($key, $data['oauth2_access_token']); + $data['oauth2_refresh_token'] = $AES->decrypt($key, $data['oauth2_refresh_token']); + } + } + + return $data; + } + + return false; + } + + abstract protected function _oauth2Load($app_username, $app_tenant); + + public function oauth2AccessWrite($key, $app_username, $app_tenant, $access_token, $refresh_token, $access_token_expire, $refresh_token_expire, $realm, $flavor = '') + { + if (strlen($key) > 0) + { + $AES = QuickBooks_Encryption_Factory::create('aes'); + + $encrypted_access_token = $AES->encrypt($key, $access_token); + $encrypted_refresh_token = $AES->encrypt($key, $refresh_token); + } + else + { + $encrypted_access_token = $access_token; + $encrypted_refresh_token = $refresh_token; + } + + return $this->_oauth2AccessWrite($app_username, $app_tenant, $encrypted_access_token, $encrypted_refresh_token, $access_token_expire, $refresh_token_expire, $realm, $flavor); + } + + abstract protected function _oauth2AccessWrite($app_username, $app_tenant, $access_token, $refresh_token, $access_token_expire, $refresh_token_expire, $realm, $flavor = ''); + + public function oauth2AccessDelete($app_username, $app_tenant) + { + return $this->_oauth2AccessDelete($app_username, $app_tenant); + } + + abstract protected function _oauth2AccessDelete($app_username, $app_tenant); + + abstract protected function _oauth2RequestResolve($app_username, $app_tenant); + + /** * Log a message to the QuickBooks log * * @param string $msg The message to place in the log diff --git a/QuickBooks/Driver/Sql.php b/QuickBooks/Driver/Sql.php index 959e7fb6..66588e97 100755 --- a/QuickBooks/Driver/Sql.php +++ b/QuickBooks/Driver/Sql.php @@ -203,6 +203,12 @@ */ define('QUICKBOOKS_DRIVER_SQL_OAUTHTABLE', 'oauth'); +/** + * Default table for OAuth2 stuff + * @var string + */ +define('QUICKBOOKS_DRIVER_SQL_OAUTH2TABLE', 'oauth2'); + /** * * @@ -2199,7 +2205,7 @@ protected function _oauthRequestWrite($app_username, $app_tenant, $token, $token } } - protected function _oauthAccessWrite($request_token, $token, $token_secret, $realm, $flavor) + protected function _oauthAccessWrite($request_token, $token, $token_secret, $realm, $flavor = '') { $errnum = 0; $errmsg = ''; @@ -2257,6 +2263,116 @@ protected function _oauthAccessDelete($app_username, $app_tenant) return $this->affected() > 0; } + /** OAuth2 functions **/ + + protected function _oauth2Load($app_username, $app_tenant) + { + $errnum = 0; + $errmsg = ''; + + $tableName = $this->_mapTableName(QUICKBOOKS_DRIVER_SQL_OAUTH2TABLE); + + if ($arr = $this->fetch($this->query(" + SELECT + * + FROM + " . $tableName . " + WHERE + app_username = '%s' AND app_tenant = '%s' ", $errnum, $errmsg, null, null, array( $app_username, $app_tenant )))) + { + $this->query(" + UPDATE + " . $tableName . " + SET + touch_datetime = '%s' + WHERE + app_username = '%s' AND app_tenant = '%s' ", $errnum, $errmsg, null, null, array( date('Y-m-d H:i:s'), $app_username, $app_tenant )); + } + + return $arr; + } + + protected function _oauth2AccessWrite($app_username, $app_tenant, $access_token, $refresh_token, $access_token_expire, $refresh_token_expire, $realm, $flavor = '') + { + $errnum = 0; + $errmsg = ''; + + $dateTime = date('Y-m-d H:i:s'); + // Check if it exists or not first + $vars = array( $app_username, $app_tenant, $access_token, $refresh_token, $access_token_expire, $refresh_token_expire, $realm, $dateTime, $dateTime ); + + $more = ""; + + if ($flavor) + { + $more .= ", qb_flavor = '%s' "; + $vars[] = $flavor; + } + + $tableName = $this->_mapTableName(QUICKBOOKS_DRIVER_SQL_OAUTH2TABLE); + + $query = " + SET + app_username = '%s', + app_tenant = '%s', + oauth2_access_token = '%s', + oauth2_refresh_token = '%s', + oauth2_access_token_expires = '%s', + oauth2_refresh_token_expires = '%s', + qb_realm = '%s', + request_datetime = '%s', + access_datetime = '%s' + "; + if (!$this->_oauth2RequestResolve($app_username, $app_tenant)) + { + // Not exists... INSERT! + $query = "INSERT INTO " . $tableName . $query . $more; + } + else + { + // Exists... Update! + $vars[] = $app_username; + $vars[] = $app_tenant; + + $query = "UPDATE " . $tableName . $query . $more . " + WHERE app_username = '%s' + AND app_tenant = '%s' + "; + } + + return $this->query($query, $errnum, $errmsg, null, null, $vars); + } + + protected function _oauth2AccessDelete($app_username, $app_tenant) + { + $errnum = 0; + $errmsg = ''; + + // Exists... DELETE! + $this->query(" + DELETE FROM + " . $this->_mapTableName(QUICKBOOKS_DRIVER_SQL_OAUTH2TABLE) . " + WHERE + app_username = '%s' AND + app_tenant = '%s' ", $errnum, $errmsg, null, null, array( $app_username, $app_tenant )); + + return $this->affected() > 0; + } + + protected function _oauth2RequestResolve($app_username, $app_tenant) + { + $errnum = 0; + $errmsg = ''; + + return $this->fetch($this->query(" + SELECT + * + FROM + " . $this->_mapTableName(QUICKBOOKS_DRIVER_SQL_OAUTH2TABLE) . " + WHERE + app_username = '%s' AND app_tenant = '%s' ", $errnum, $errmsg, null, null, array( $app_username, $app_tenant ))); + } + /** * Write a message to the log file * @@ -3052,6 +3168,28 @@ protected function _initialize($init_options = array()) //print_r($arr_sql); //exit; + $table = $this->_mapTableName(QUICKBOOKS_DRIVER_SQL_OAUTH2TABLE); + $def = array( + 'quickbooks_oauth2_id' => array( QUICKBOOKS_DRIVER_SQL_SERIAL ), + 'app_username' => array( QUICKBOOKS_DRIVER_SQL_VARCHAR, 255 ), + 'app_tenant' => array( QUICKBOOKS_DRIVER_SQL_VARCHAR, 255 ), + 'oauth2_access_token' => array( QUICKBOOKS_DRIVER_SQL_VARCHAR, 1024, 'null' ), + 'oauth2_refresh_token' => array( QUICKBOOKS_DRIVER_SQL_VARCHAR, 1024, 'null' ), + 'oauth2_access_token_expires' => array( QUICKBOOKS_DRIVER_SQL_DATETIME, null, 'null' ), + 'oauth2_refresh_token_expires' => array( QUICKBOOKS_DRIVER_SQL_DATETIME, null, 'null' ), + 'qb_realm' => array( QUICKBOOKS_DRIVER_SQL_VARCHAR, 32, 'null' ), + 'qb_flavor' => array( QUICKBOOKS_DRIVER_SQL_VARCHAR, 12, 'null' ), + 'qb_user' => array( QUICKBOOKS_DRIVER_SQL_VARCHAR, 64, 'null' ), + 'request_datetime' => array( QUICKBOOKS_DRIVER_SQL_DATETIME ), + 'access_datetime' => array( QUICKBOOKS_DRIVER_SQL_DATETIME, null, 'null' ), + 'touch_datetime' => array( QUICKBOOKS_DRIVER_SQL_DATETIME, null, 'null' ), + ); + $primary = 'quickbooks_oauth2_id'; + $keys = array( ); + $uniques = array( array( 'app_username', 'app_tenant' ) ); + + $arr_sql = array_merge($arr_sql, $this->_generateCreateTable($table, $def, $primary, $keys, $uniques)); + // Support for mirroring the QuickBooks database in an SQL database if ($config['quickbooks_sql_enabled']) { diff --git a/QuickBooks/HTTP.php b/QuickBooks/HTTP.php index 6b154a70..da76ece0 100644 --- a/QuickBooks/HTTP.php +++ b/QuickBooks/HTTP.php @@ -1,14 +1,14 @@ - * + * * @package QuickBooks * @subpackage HTTP */ @@ -50,84 +50,85 @@ class QuickBooks_HTTP { const HTTP_400 = 400; const HTTP_401 = 401; + const HTTP_404 = 404; const HTTP_500 = 500; protected $_url; - + protected $_request_headers; - + protected $_body; - + protected $_post; - + protected $_get; - + protected $_last_response; - + protected $_last_request; - + protected $_last_duration; protected $_last_info; - + protected $_errnum; - + protected $_errmsg; - + protected $_verify_peer; - + protected $_verify_host; - + protected $_certificate; - + protected $_masking; - + protected $_log; - + protected $_debug; - + protected $_test; - + protected $_return_headers; - + /** * A variable indicating whether or not to make a synchronous request * @var boolean */ protected $_sync; - + /** * Create a new QuickBooks_HTTP object instance to make HTTP requests to remote URLs - * + * * @param string $url The URL to make an HTTP request to */ public function __construct($url = null) { $this->_url = $url; - + $this->_verify_peer = true; $this->_verify_host = true; - + $this->_masking = true; - + $this->_log = ''; - + $this->_debug = false; $this->_test = false; - + $this->_sync = true; - + $this->_request_headers = array(); $this->_return_headers = false; - + $this->_last_request = null; $this->_last_response = null; $this->_last_duration = 0.0; } - + /** * Set the URL - * + * * @param string $url * @return void */ @@ -135,10 +136,10 @@ public function setURL($url) { $this->_url = $url; } - + /** - * Get the URL - * + * Get the URL + * * @return string */ public function getURL() @@ -146,39 +147,39 @@ public function getURL() // @TODO Support for query string args from ->setGETValues() return $this->_url; } - + public function verifyPeer($yes_or_no) { $this->_verify_peer = (boolean) $yes_or_no; } - + public function verifyHost($yes_or_no) { $this->_verify_host = (boolean) $yes_or_no; } - + public function setHeaders($arr) { foreach ($arr as $key => $value) { - if (is_numeric($key) and + if (is_numeric($key) and false !== ($pos = strpos($value, ':'))) { // 0 => "Header: value" format - + $key = substr($value, 0, $pos); $value = ltrim(substr($value, $pos + 1)); } - + // "Header" => "value" format - + $this->setHeader($key, $value); } } - + /** - * Tell whether or not to return the HTTP response headers - * + * Tell whether or not to return the HTTP response headers + * * @param boolean $return * @return void */ @@ -186,12 +187,12 @@ public function returnHeaders($return) { $this->_return_headers = (boolean) $return; } - + public function setHeader($key, $value) { $this->_request_headers[$key] = $value; } - + public function getHeaders($as_combined_array = false) { if ($as_combined_array) @@ -201,41 +202,41 @@ public function getHeaders($as_combined_array = false) { $list[] = $key . ': ' . $value; } - + return $list; } - + return $this->_request_headers; } - + public function getHeader($key) { if (isset($this->_request_headers[$key])) { return $this->_request_headers[$key]; } - + return null; } - + public function setRawBody($str) { $this->_body = $str; } - + public function setPOSTValues($arr) { $this->_post = $arr; } - + public function setGETValues($arr) { $this->_get = $arr; } - + /** - * - * + * + * * @return string */ public function getRawBody() @@ -248,46 +249,46 @@ public function getRawBody() { return http_build_query($this->_post); } - + return ''; } - + public function setCertificate($cert) { $this->_certificate = $cert; } - + public function GET() { return $this->_request(QUICKBOOKS_HTTP_METHOD_GET); } - + public function POST() { return $this->_request(QUICKBOOKS_HTTP_METHOD_POST); } - + public function DELETE() { return $this->_request(QUICKBOOKS_HTTP_METHOD_DELETE); } - + public function HEAD() { return $this->_request(QUICKBOOKS_HTTP_METHOD_HEAD); } - + public function useDebugMode($yes_or_no) { $prev = $this->_debug; $this->_debug = (boolean) $yes_or_no; - + return $prev; } /** * If masking is enabled (default) then credit card numbers, connection tickets, and session tickets will be masked when output or logged - * + * * @param boolean $yes_or_no * @return void */ @@ -295,53 +296,53 @@ public function useMasking($yes_or_no) { $this->_masking = (boolean) $yes_or_no; } - + public function useTestEnvironment($yes_or_no) { $prev = $this->_test; $this->_test = (boolean) $yes_or_no; - + return $prev; } - + public function useLiveEnvironment($yes_or_no) { $prev = $this->_test; $this->_test = ! (boolean) $yes_or_no; - + return $prev; } - + /** * Get the error number of the last error that occured - * + * * @return integer */ public function errorNumber() { return $this->_errnum; } - + /** * Get the error message of the last error that occured - * + * * @return string */ public function errorMessage() { return $this->_errmsg; } - + /** * Get the last raw XML response that was received - * + * * @return string */ public function lastResponse() { return $this->_last_response; } - + /** * Get the last raw XML request that was sent * @@ -353,7 +354,7 @@ public function lastRequest() } /** - * + * * */ public function lastDuration() @@ -365,10 +366,10 @@ public function lastInfo() { return $this->_last_info; } - + /** * Set an error message - * + * * @param integer $errnum The error number/code * @param string $errmsg The text error message * @return void @@ -378,11 +379,11 @@ protected function _setError($errnum, $errmsg = '') $this->_errnum = $errnum; $this->_errmsg = $errmsg; } - + /** - * - * - * + * + * + * * @param string $message * @param integer $level * @return boolean @@ -398,48 +399,48 @@ protected function _log($message) { print($message . QUICKBOOKS_CRLF); } - - // + + // $this->_log .= $message . QUICKBOOKS_CRLF; - + return true; } - + public function resetLog() { $this->_log = ''; } - + public function getLog() { return $this->_log; } - + /** - * + * * @todo Implement support for asynchronous requests - * + * */ public function setSynchronous($yes_or_no) { $this->_sync = (boolean) $yes_or_no; } - + public function setAsynchronous($yes_or_no) { $this->_sync = !( (boolean) $yes_or_no); } - + /** - * Make an HTTP request - * + * Make an HTTP request + * * @param string $method * @return string */ protected function _request($method) { $start = microtime(true); - + if (!function_exists('curl_init')) { die('You must have the PHP cURL extension (php.net/curl) enabled to use this (' . QUICKBOOKS_PACKAGE_NAME . ' v' . QUICKBOOKS_PACKAGE_VERSION . ').'); @@ -447,15 +448,15 @@ protected function _request($method) $this->_log('Using CURL to send request!', QUICKBOOKS_LOG_DEVELOP); $return = $this->_requestCurl($method, $errnum, $errmsg); - + if ($errnum) { $this->_setError($errnum, $errmsg); } - + // Calculate and set how long the last HTTP request/response took to make $this->_last_duration = microtime(true) - $start; - + return $return; } @@ -463,13 +464,13 @@ protected function _requestCurl($method, &$errnum, &$errmsg) { $url = $this->getURL(); $raw_body = $this->getRawBody(); - + $headers = $this->getHeaders(true); - + $this->_log('Opening connection to: ' . $url, QUICKBOOKS_LOG_VERBOSE); - + $params = array(); - + if ($method == QUICKBOOKS_HTTP_METHOD_POST) { $headers[] = 'Content-Length: ' . strlen($raw_body); @@ -483,7 +484,7 @@ protected function _requestCurl($method, &$errnum, &$errmsg) $query = '?' . http_build_query($this->_get); } - if ($qs = parse_url($url, PHP_URL_QUERY) and + if ($qs = parse_url($url, PHP_URL_QUERY) and false !== strpos($qs, ' ')) { $url = str_replace($qs, str_replace(' ', '+', $qs), $url); @@ -496,25 +497,25 @@ protected function _requestCurl($method, &$errnum, &$errmsg) //$params[CURLOPT_TIMEOUT] = 15; $params[CURLOPT_HTTPHEADER] = $headers; $params[CURLOPT_ENCODING] = ''; // This makes it say it supports gzip *and* deflate - + $params[CURLOPT_VERBOSE] = $this->_debug; - + if ($this->_return_headers) { $params[CURLOPT_HEADER] = true; } - + // Some Windows servers will fail with SSL errors unless we turn this off if (!$this->_verify_peer) { $params[CURLOPT_SSL_VERIFYPEER] = false; } - + if (!$this->_verify_host) { $params[CURLOPT_SSL_VERIFYHOST] = 0; } - + // Check for an SSL certificate (HOSTED model of communication) if ($this->_certificate) { @@ -526,17 +527,17 @@ protected function _requestCurl($method, &$errnum, &$errmsg) else { $msg = 'Specified SSL certificate could not be located: ' . $this->_certificate; - + $this->_log($msg, QUICKBOOKS_LOG_NORMAL); $errnum = QUICKBOOKS_HTTP_ERROR_CERTIFICATE; $errmsg = $msg; return false; } } - + // Fudge the outgoing request because CURL won't give us it $request = ''; - + if ($method == QUICKBOOKS_HTTP_METHOD_POST) { $request .= 'POST '; @@ -555,28 +556,28 @@ protected function _requestCurl($method, &$errnum, &$errmsg) $request .= 'GET '; } $request .= $params[CURLOPT_URL] . ' HTTP/1.1' . "\r\n"; - + foreach ($headers as $header) { $request .= $header . "\r\n"; } $request .= "\r\n"; $request .= $this->getRawBody(); - + $this->_log('CURL options: ' . print_r($params, true), QUICKBOOKS_LOG_DEBUG); - + $this->_last_request = $request; $this->_log('HTTP request: ' . $request, QUICKBOOKS_LOG_DEBUG); // Set as DEBUG so that no one accidentally logs all the credit card numbers... - + $ch = curl_init(); curl_setopt_array($ch, $params); $response = curl_exec($ch); - + /* print("\n\n\n" . '---------------------' . "\n"); print('[[request ' . $request . ']]' . "\n\n\n"); print('[[resonse ' . $response . ']]' . "\n\n\n\n\n"); - + print_r($params); print_r(curl_getinfo($ch)); print_r($headers); @@ -585,22 +586,22 @@ protected function _requestCurl($method, &$errnum, &$errmsg) $this->_last_response = $response; $this->_log('HTTP response: ' . substr($response, 0, 500) . '...', QUICKBOOKS_LOG_VERBOSE); - + $this->_last_info = curl_getinfo($ch); - if (curl_errno($ch)) + if (curl_errno($ch)) { $errnum = curl_errno($ch); $errmsg = curl_error($ch); $this->_log('CURL error: ' . $errnum . ': ' . $errmsg, QUICKBOOKS_LOG_NORMAL); - + return false; - } - - // Close the connection + } + + // Close the connection @curl_close($ch); - + return $response; } } diff --git a/QuickBooks/IPP.php b/QuickBooks/IPP.php index becf8916..73fdeb31 100755 --- a/QuickBooks/IPP.php +++ b/QuickBooks/IPP.php @@ -34,6 +34,9 @@ // OAuth QuickBooks_Loader::load('/QuickBooks/IPP/OAuth.php'); +// OAuth2 +QuickBooks_Loader::load('/QuickBooks/IPP/OAuth2.php'); + // IntuitAnywhere widgets QuickBooks_Loader::load('/QuickBooks/IPP/IntuitAnywhere.php'); @@ -88,7 +91,8 @@ class QuickBooks_IPP const API_GETENTITLEMENTVALUESANDUSERROLE = 'API_GetEntitlementValuesAndUserRole'; const AUTHMODE_FEDERATED = 'federated'; - const AUTHMODE_OAUTH = 'oauth'; + const AUTHMODE_OAUTH = 'oauth'; + const AUTHMODE_OAUTH2 = 'oauth2'; /** * @@ -381,6 +385,11 @@ public function context($ticket = null, $token = null, $check_if_valid = true) // @todo Support for checking if it's valid or not } + elseif ($this->_authmode == QuickBooks_IPP::AUTHMODE_OAUTH2) + { + $Context = new QuickBooks_IPP_Context($this, null, $token); + + } else { if (is_null($ticket)) @@ -510,7 +519,7 @@ public function authcreds() } /** - * Set the authorization mode for HTTP requests (Federated, or OAuth) + * Set the authorization mode for HTTP requests (Federated, or OAuth, OAuth2) * * @param string $authmode The new auth mode * @return string The currently set auth mode @@ -1618,6 +1627,30 @@ protected function _request($Context, $type, $url, $action, $data, $post = true) } } } + elseif ($this->_authmode == QuickBooks_IPP::AUTHMODE_OAUTH2) + { + // If we have credentials, sign the request + if ($this->_authcred['oauth2_access_token'] and + $this->_authcred['oauth2_refresh_token']) + { + // Sign the request + $OAuth = new QuickBooks_IPP_OAuth2($this->_authcred['oauth2_client_id'], $this->_authcred['oauth2_client_secret'], $this->_authcred['qb_realm'], $this->_authcred['oauth2_access_token'], $this->_authcred['oauth2_refresh_token']); + + if ($post) + { + $action = QuickBooks_IPP_OAuth2::METHOD_POST; + } + else + { + $action = QuickBooks_IPP_OAuth2::METHOD_GET; + } + + $sign = $OAuth->sign($action, $url); + + // Always use the header, regardless of POST or GET + $headers['Authorization'] = $sign; + } + } else if (is_object($Context)) { // FEDERATED authentication diff --git a/QuickBooks/IPP/IntuitAnywhere.php b/QuickBooks/IPP/IntuitAnywhere.php index 93e85bc1..8a931d6a 100644 --- a/QuickBooks/IPP/IntuitAnywhere.php +++ b/QuickBooks/IPP/IntuitAnywhere.php @@ -15,40 +15,20 @@ * @package QuickBooks */ -class QuickBooks_IPP_IntuitAnywhere +// Base class with common functions for OAuth1 and OAuth1 implementations +QuickBooks_Loader::load('/QuickBooks/IPP/IntuitAnywhereBase.php'); + +class QuickBooks_IPP_IntuitAnywhere extends QuickBooks_IPP_IntuitAnywhereBase { - protected $_this_url; - protected $_that_url; - protected $_consumer_key; protected $_consumer_secret; - - protected $_errnum; - protected $_errmsg; - - protected $_debug; - - protected $_driver; - - protected $_crypt; - - protected $_key; - protected $_last_request; - protected $_last_response; - const URL_REQUEST_TOKEN = 'https://oauth.intuit.com/oauth/v1/get_request_token'; const URL_ACCESS_TOKEN = 'https://oauth.intuit.com/oauth/v1/get_access_token'; const URL_CONNECT_BEGIN = 'https://appcenter.intuit.com/Connect/Begin'; const URL_CONNECT_DISCONNECT = 'https://appcenter.intuit.com/api/v1/Connection/Disconnect'; const URL_CONNECT_RECONNECT = 'https://appcenter.intuit.com/api/v1/Connection/Reconnect'; - const URL_APP_MENU = 'https://appcenter.intuit.com/api/v1/Account/AppMenu'; - const EXPIRY_EXPIRED = 'expired'; - const EXPIRY_NOTYET = 'notyet'; - const EXPIRY_SOON = 'soon'; - const EXPIRY_UNKNOWN = 'unknown'; - /** * * @@ -70,148 +50,20 @@ public function __construct($dsn, $encryption_key, $consumer_key, $consumer_secr $this->_consumer_secret = $consumer_secret; $this->_debug = false; - } - - /** - * Turn on/off debug mode - * - * @param boolean $true_or_false - */ - public function useDebugMode($true_or_false) - { - $this->_debug = (boolean) $true_or_false; - } - - /** - * Get the last error number - * - * @return integer - */ - public function errorNumber() - { - return $this->_errnum; - } - - /** - * Get the last error message - * - * @return string - */ - public function errorMessage() - { - return $this->_errmsg; - } - - /** - * Set an error message - * - * @param integer $errnum The error number/code - * @param string $errmsg The text error message - * @return void - */ - protected function _setError($errnum, $errmsg = '') - { - $this->_errnum = $errnum; - $this->_errmsg = $errmsg; - } - - public function lastRequest() - { - return $this->_last_request; - } - - public function lastResponse() - { - return $this->_last_response; - } - - /** - * Returns TRUE if an OAuth token exists for this user, FALSE otherwise - * - * @param string $app_username - * @return bool - */ - public function check($app_username, $app_tenant) - { - if ($arr = $this->load($app_username, $app_tenant)) - { - return true; - } - - return false; - } - - /** - * Test to see if a connection actually works (make sure you haven't been disconnected on Intuit's end) - * - */ - public function test($app_username, $app_tenant) - { - if ($creds = $this->load($app_username, $app_tenant)) - { - $IPP = new QuickBooks_IPP(); - - $IPP->authMode( - QuickBooks_IPP::AUTHMODE_OAUTH, - $app_username, - $creds); - - if ($Context = $IPP->context()) - { - // Set the DBID - $IPP->dbid($Context, 'something'); - - // Set the IPP flavor - $IPP->flavor($creds['qb_flavor']); - - // Get the base URL if it's QBO - if ($creds['qb_flavor'] == QuickBooks_IPP_IDS::FLAVOR_ONLINE) - { - $cur_version = $IPP->version(); - - $IPP->version(QuickBooks_IPP_IDS::VERSION_3); // Need v3 for this - - $CustomerService = new QuickBooks_IPP_Service_Customer(); - $customers = $CustomerService->query($Context, $creds['qb_realm'], "SELECT * FROM Customer MAXRESULTS 1"); - - $IPP->version($cur_version); // Revert back to whatever they set - - //$IPP->baseURL($IPP->getBaseURL($Context, $creds['qb_realm'])); - } - else - { - $companies = $IPP->getAvailableCompanies($Context); - } - - //print('[[' . $IPP->lastRequest() . ']]' . "\n\n"); - //print('[[' . $IPP->lastResponse() . ']]' . "\n\n"); - //print('here we are! [' . $IPP->errorCode() . ']'); - - // Check the last error code now... - if ($IPP->errorCode() == 401 or // most calls return this - $IPP->errorCode() == 3200) // but for some stupid reason the getAvailableCompanies call returns this - { - return false; - } - - return true; - } - } - - return false; + $this->_auth_mode = QuickBooks_IPP::AUTHMODE_OAUTH; } /** * Load OAuth credentials from the database * * @param string $app_username + * @param string $app_tenant + * * @return array */ public function load($app_username, $app_tenant) { - if ($arr = $this->_driver->oauthLoad($this->_key, $app_username, $app_tenant) and - strlen($arr['oauth_access_token']) > 0 and - strlen($arr['oauth_access_token_secret']) > 0) + if ($arr = $this->_loadSettings($this->_key, $app_username, $app_tenant)) { $arr['oauth_consumer_key'] = $this->_consumer_key; $arr['oauth_consumer_secret'] = $this->_consumer_secret; @@ -228,15 +80,13 @@ public function load($app_username, $app_tenant) * @param string $app_username * @param string $app_tenant * @param integer $within - * @return One of the QuickBooks_IPP_IntuitAnywhere::EXPIRY_* constants + * @return string One of the QuickBooks_IPP_IntuitAnywhere::EXPIRY_* constants */ public function expiry($app_username, $app_tenant, $within = 2592000) { $lifetime = 15552000; - if ($arr = $this->_driver->oauthLoad($this->_key, $app_username, $app_tenant) and - strlen($arr['oauth_access_token']) > 0 and - strlen($arr['oauth_access_token_secret']) > 0) + if ($arr = $this->_loadSettings($this->_key, $app_username, $app_tenant)) { $expires = $lifetime + strtotime($arr['access_datetime']); @@ -268,12 +118,12 @@ public function expiry($app_username, $app_tenant, $within = 2592000) * * @param string $app_username * @param string $app_tenant + * + * @return bool */ public function reconnect($app_username, $app_tenant) { - if ($arr = $this->_driver->oauthLoad($this->_key, $app_username, $app_tenant) and - strlen($arr['oauth_access_token']) > 0 and - strlen($arr['oauth_access_token_secret']) > 0) + if ($arr = $this->_loadSettings($this->_key, $app_username, $app_tenant)) { $arr['oauth_consumer_key'] = $this->_consumer_key; $arr['oauth_consumer_secret'] = $this->_consumer_secret; @@ -310,13 +160,13 @@ public function reconnect($app_username, $app_tenant) return true; } } + + return false; } public function disconnect($app_username, $app_tenant, $force = false) { - if ($arr = $this->_driver->oauthLoad($this->_key, $app_username, $app_tenant) and - strlen($arr['oauth_access_token']) > 0 and - strlen($arr['oauth_access_token_secret']) > 0) + if ($arr = $this->_loadSettings($this->_key, $app_username, $app_tenant)) { $arr['oauth_consumer_key'] = $this->_consumer_key; $arr['oauth_consumer_secret'] = $this->_consumer_secret; @@ -596,6 +446,27 @@ protected function _request($method, $url, $params = array(), $token = null, $se $this->_setError(QuickBooks_IPP::ERROR_OK, ''); return $return; } + + /** + * Load settings + * + * @param string $key + * @param string $app_username + * @param string $app_tenant + * + * @return mixed + */ + protected function _loadSettings($key, $app_username, $app_tenant) { + $arr = $this->_driver->oauthLoad($key, $app_username, $app_tenant); + + if ($arr && strlen($arr['oauth_access_token']) > 0 and + strlen($arr['oauth_access_token_secret']) > 0 + ) { + return $arr; + } + + return false; + } } diff --git a/QuickBooks/IPP/IntuitAnywhereBase.php b/QuickBooks/IPP/IntuitAnywhereBase.php new file mode 100644 index 00000000..df954ef0 --- /dev/null +++ b/QuickBooks/IPP/IntuitAnywhereBase.php @@ -0,0 +1,234 @@ + + * @license LICENSE.txt + * + * @package QuickBooks + */ + +abstract class QuickBooks_IPP_IntuitAnywhereBase +{ + protected $_this_url; + protected $_that_url; + + protected $_errnum; + protected $_errmsg; + + protected $_debug; + + /** + * @var QuickBooks_Driver + */ + protected $_driver; + + protected $_crypt; + + protected $_key; + + protected $_last_request; + protected $_last_response; + + protected $_auth_mode; + + const URL_APP_MENU = 'https://appcenter.intuit.com/api/v1/Account/AppMenu'; + + const EXPIRY_EXPIRED = 'expired'; + const EXPIRY_NOTYET = 'notyet'; + const EXPIRY_SOON = 'soon'; + const EXPIRY_UNKNOWN = 'unknown'; + + /** + * Turn on/off debug mode + * + * @param boolean $true_or_false + */ + public function useDebugMode($true_or_false) + { + $this->_debug = (boolean) $true_or_false; + } + + /** + * Get the last error number + * + * @return integer + */ + public function errorNumber() + { + return $this->_errnum; + } + + /** + * Get the last error message + * + * @return string + */ + public function errorMessage() + { + return $this->_errmsg; + } + + /** + * Set an error message + * + * @param integer $errnum The error number/code + * @param string $errmsg The text error message + * @return void + */ + protected function _setError($errnum, $errmsg = '') + { + $this->_errnum = $errnum; + $this->_errmsg = $errmsg; + } + + public function lastRequest() + { + return $this->_last_request; + } + + public function lastResponse() + { + return $this->_last_response; + } + + /** + * Returns TRUE if an OAuth token exists for this user, FALSE otherwise + * + * @param string $app_username + * @param string $app_tenant + * + * @return bool + */ + public function check($app_username, $app_tenant) + { + if ($arr = $this->load($app_username, $app_tenant)) + { + return true; + } + + return false; + } + + /** + * Test to see if a connection actually works (make sure you haven't been disconnected on Intuit's end) + * + * @param string $app_username + * @param string $app_tenant + * + * @return bool + */ + public function test($app_username, $app_tenant) + { + if ($creds = $this->load($app_username, $app_tenant)) + { + $IPP = new QuickBooks_IPP(); + + $IPP->authMode( + isset($this->_auth_mode) ? $this->_auth_mode : QuickBooks_IPP::AUTHMODE_OAUTH, + $app_username, + $creds); + + if ($Context = $IPP->context()) + { + // Set the DBID + $IPP->dbid($Context); + + // Set the IPP flavor + $IPP->flavor($creds['qb_flavor']); + + // Get the base URL if it's QBO + if ($creds['qb_flavor'] == QuickBooks_IPP_IDS::FLAVOR_ONLINE) + { + $cur_version = $IPP->version(); + + $IPP->version(QuickBooks_IPP_IDS::VERSION_3); // Need v3 for this + + $CustomerService = new QuickBooks_IPP_Service_Customer(); + $CustomerService->query( + $Context, + $creds['qb_realm'], + "SELECT * FROM Customer MAXRESULTS 1" + ); + + $IPP->version($cur_version); // Revert back to whatever they set + + //$IPP->baseURL($IPP->getBaseURL($Context, $creds['qb_realm'])); + } + else + { + $IPP->getAvailableCompanies($Context); + } + + //print('[[' . $IPP->lastRequest() . ']]' . "\n\n"); + //print('[[' . $IPP->lastResponse() . ']]' . "\n\n"); + //print('here we are! [' . $IPP->errorCode() . ']'); + + // Check the last error code now... + if ($IPP->errorCode() == 401 or // most calls return this + $IPP->errorCode() == 3200) // but for some stupid reason the getAvailableCompanies call returns this + { + return false; + } + + return true; + } + } + + return false; + } + + /** + * Load OAuth credentials from the database + * + * @param string $app_username + * @param string $app_tenant + * + * @return array + */ + abstract public function load($app_username, $app_tenant); + + /** + * Check whether a connection is due for refresh/reconnect + * + * @param string $app_username + * @param string $app_tenant + * @param integer $within + * @return string One of the QuickBooks_IPP_IntuitAnywhere::EXPIRY_* constants + */ + abstract public function expiry($app_username, $app_tenant, $within = 2592000); + + /** + * Reconnect/refresh the OAuth tokens + * + * For this to succeed, the token expiration must be within 30 days of the + * date that this method is called (6 months after original token was + * created). This is an Intuit-imposed security restriction. Calls outside + * of that date range will fail with an error. + * + * @param string $app_username + * @param string $app_tenant + */ + abstract public function reconnect($app_username, $app_tenant); + + abstract public function disconnect($app_username, $app_tenant, $force = false); + + /** + * Handle an OAuth request login thing + * + * @param string $app_username + * @param string $app_tenant + */ + abstract public function handle($app_username, $app_tenant); + + abstract protected function _loadSettings($key, $app_username, $app_tenant); +} + + diff --git a/QuickBooks/IPP/IntuitAnywhereOAuth2.php b/QuickBooks/IPP/IntuitAnywhereOAuth2.php new file mode 100644 index 00000000..4e122a59 --- /dev/null +++ b/QuickBooks/IPP/IntuitAnywhereOAuth2.php @@ -0,0 +1,387 @@ + + * @license LICENSE.txt + * + * @package QuickBooks + */ + +// Base class shared between OAuth1 and OAuth2 implementations +QuickBooks_Loader::load('/QuickBooks/IPP/IntuitAnywhereBase.php'); + +// OAuth2 Helper, handles mostly keys operations +QuickBooks_Loader::load('/QuickBooks/IPP/OAuth2Helper.php'); + +/** + * Class QuickBooks_IPP_IntuitAnywhere_OAuth2 + * + * @author Evgeniy Bogdanov + */ +class QuickBooks_IPP_IntuitAnywhereOAuth2 extends QuickBooks_IPP_IntuitAnywhereBase +{ + /** + * ClientId + * + * @var string + */ + private $_client_id; + + /** + * ClientSecret + * + * @var string + */ + private $_client_secret; + + /** + * + * @param string $dsn + * @param string $encryption_key + * + * @param string $client_id The OAuth2 Client Id key Intuit gives you + * @param string $client_secret The OAuth2 Client secret Intuit gives you + * @param string $this_url The URL of your QuickBooks_IntuitAnywhere class instance + * @param string $that_url The URL the user should be sent to after authenticated + */ + public function __construct($dsn, $encryption_key, $client_id, $client_secret, $this_url = null, $that_url = null) + { + $this->_driver = QuickBooks_Driver_Factory::create($dsn); + + $this->_key = $encryption_key; + + $this->_this_url = $this_url; + $this->_that_url = $that_url; + + $this->_client_id = $client_id; + $this->_client_secret = $client_secret; + + $this->_auth_mode = QuickBooks_IPP::AUTHMODE_OAUTH2; + + $this->_debug = false; + } + + /** + * Load OAuth credentials from the database + * + * @param string $app_username + * @param string $app_tenant + * + * @return array|false + */ + public function load($app_username, $app_tenant) + { + if ($arr = $this->_loadSettings($this->_key, $app_username, $app_tenant)) + { + $arr['oauth2_client_id'] = $this->_client_id; + $arr['oauth2_client_secret'] = $this->_client_secret; + + return $arr; + } + + return false; + } + + /** + * Check whether a connection is due for refresh/reconnect + * + * @param string $app_username + * @param string $app_tenant + * @param integer $within + * + * @return string One of the QuickBooks_IPP_IntuitAnywhere::EXPIRY_* constants + */ + public function expiry($app_username, $app_tenant, $within = 600) + { + if ($arr = $this->_loadSettings($this->_key, $app_username, $app_tenant)) + { + return $this->_expiry($arr, $within); + } + + return QuickBooks_IPP_IntuitAnywhere::EXPIRY_UNKNOWN; + } + + /** + * Perform logic check if about token state + * + * @param array $arr + * @param integer $within (seconds of still alive token, but we consider it as expiring soon) + * + * @return string + */ + protected function _expiry($arr, $within = 600) + { + if (!empty($arr)) + { + $expires = strtotime($arr['oauth2_access_token_expires']); + + $diff = $expires - time(); + + if ($diff < 0) + { + // Already expired + return QuickBooks_IPP_IntuitAnywhere::EXPIRY_EXPIRED; + } + else if ($diff < $within) + { + return QuickBooks_IPP_IntuitAnywhere::EXPIRY_SOON; + } + + return QuickBooks_IPP_IntuitAnywhere::EXPIRY_NOTYET; + } + + return QuickBooks_IPP_IntuitAnywhere::EXPIRY_UNKNOWN; + } + + /** + * Reconnect/refresh the OAuth tokens + * + * For this to succeed, the token expiration must be within 30 days of the + * date that this method is called (6 months after original token was + * created). This is an Intuit-imposed security restriction. Calls outside + * of that date range will fail with an error. + * + * @param string $app_username + * @param string $app_tenant + * + * @return bool + */ + public function reconnect($app_username, $app_tenant) + { + if ($arr = $this->_loadSettings($this->_key, $app_username, $app_tenant)) + { + try + { + $oauth2Token = new QuickBooks_IPP_OAuth2( + $this->_client_id, + $this->_client_secret, + $arr['qb_realm'], + $arr['oauth2_access_token'], + $arr['oauth2_refresh_token'] + ); + + $oauth2Helper = new QuickBooks_IPP_OAuth2Helper($oauth2Token); + $newToken = $oauth2Helper->refreshToken(); + + $this->_driver->oauth2AccessDelete($app_username, $app_tenant); + + $this->_driver->oauth2AccessWrite( + $this->_key, + $app_username, + $app_tenant, + $newToken->getAccessToken(), + $newToken->getRefreshToken(), + $newToken->getAccessTokenExpiresTime(), + $newToken->getRefreshTokenExpiresTime(), + $newToken->getRealmId(), + null); + + return true; + + } + catch (\Exception $e) + { + $this->_setError($e->getCode(), $e->getMessage()); + } + } + + return false; + } + + /** + * Disconnect application + * + * @param string $app_username + * @param string $app_tenant + * @param bool $force + * @return bool + */ + public function disconnect($app_username, $app_tenant, $force = false) + { + if ($arr = $this->_loadSettings($this->_key, $app_username, $app_tenant)) + { + $oauth2Token = new QuickBooks_IPP_OAuth2( + $this->_client_id, + $this->_client_secret, + $arr['qb_realm'], + $arr['oauth2_access_token'], + $arr['oauth2_refresh_token'] + ); + + try + { + $oauth2Helper = new QuickBooks_IPP_OAuth2Helper($oauth2Token); + $code = $oauth2Helper->revokeToken(); + + if ($code == 0 or + $code == 270 or // Sometimes it returns "270: OAuth Token rejected" for some reason? + $force) + { + return $this->_driver->oauth2AccessDelete($app_username, $app_tenant); + } + } + catch (\Exception $e) + { + $this->_setError($e->getCode(), $e->getMessage()); + } + } + + return false; + } + + /** + * Handle an OAuth2 request login thing + * + * @param string $app_username + * @param string $app_tenant + * @param string $scope + * @param string $status + * + * @throws \Exception + */ + public function handle($app_username, $app_tenant, $scope = QuickBooks_IPP_OAuth2Helper::SCOPE_ACCOUNTING, $status = '') + { + if ($this->check($app_username, $app_tenant) and // We have tokens ... + $this->test($app_username, $app_tenant)) // ... and they are valid + { + // They are already logged in, send them on to exchange data + header('Location: ' . $this->_that_url); + exit; + } + else + { + $oauth2Token = new QuickBooks_IPP_OAuth2( + $this->_client_id, + $this->_client_secret + ); + $oauth2Helper = new QuickBooks_IPP_OAuth2Helper($oauth2Token); + + $realmId = filter_input(INPUT_GET, 'realmId', FILTER_SANITIZE_NUMBER_INT); + + if (isset($_GET['code']) && $realmId) + { + $token = $oauth2Helper->loadTokenByCode($_GET['code'], $this->_this_url, $realmId); + + $this->_driver->oauth2AccessWrite( + $this->_key, + $app_username, + $app_tenant, + $token->getAccessToken(), + $token->getRefreshToken(), + $token->getAccessTokenExpiresTime(), + $token->getRefreshTokenExpiresTime(), + $token->getRealmId(), + QuickBooks_IPP_IDS::FLAVOR_ONLINE); + + header('Location: ' . $this->_that_url); + exit; + } + else + { + $auth_url = $oauth2Helper->getAuthorizationURL($this->_this_url, $scope, $status); + + // Forward them to the auth page + header('Location: ' . $auth_url); + exit; + } + } + } + + /** + * Handle refresh of "expired" or "expire soon" access tokens + * + * @param string $app_username + * @param string $app_tenant + */ + public function refresh_expired_token($app_username, $app_tenant) + { + $settings = $this->load($app_username, $app_tenant); + + if (!empty($settings)) + { + $token = QuickBooks_IPP_OAuth2::fromArray($settings); + $token_state = $this->expiry($app_username, $app_tenant); + + if (in_array($token_state, array(QuickBooks_IPP_IntuitAnywhere::EXPIRY_EXPIRED, QuickBooks_IPP_IntuitAnywhere::EXPIRY_SOON))) + { + try + { + $helper = new QuickBooks_IPP_OAuth2Helper($token); + $new_token = $helper->refreshToken(); + + $this->_driver->oauth2AccessWrite( + $this->_key, + $app_username, + $app_tenant, + $new_token->getAccessToken(), + $new_token->getRefreshToken(), + $new_token->getAccessTokenExpiresTime(), + $new_token->getRefreshTokenExpiresTime(), + $new_token->getRealmId() + ); + } + catch (\Exception $e) + { + $this->_setError($e->getCode(), $e->getMessage()); + } + } + } + } + + public function widgetConnect() + { + + } + + /** + * This function returns the html for displaying the "Blue Dot" menu + * + * Blue dot menu is deprecated since June 30th, 2017 + * + * @link https://developer.intuit.com/hub/blog/2017/03/30/developer-alert-intuit-blue-dot-widget-will-deprecated-june-30th-2017 + * + * @param string $app_username + * @param string $app_tenant + * + * @return string html string + */ + public function widgetMenu($app_username, $app_tenant) + { + /** + * @kludge + * + * Nothing special in logic here, I just suppress IDE warning. Pull requests are welcome + */ + return ($app_username . $app_tenant) ? '' : ''; + } + + /** + * Load settings + * + * @param string $key + * @param string $app_username + * @param string $app_tenant + * + * @return mixed + */ + protected function _loadSettings($key, $app_username, $app_tenant) { + $return = $this->_driver->oauth2Load($key, $app_username, $app_tenant); + + if (empty($return['oauth2_access_token']) || empty($return['oauth2_refresh_token'])) + { + return false; + } + + return $return; + } +} + + diff --git a/QuickBooks/IPP/OAuth2.php b/QuickBooks/IPP/OAuth2.php new file mode 100644 index 00000000..f438fa64 --- /dev/null +++ b/QuickBooks/IPP/OAuth2.php @@ -0,0 +1,312 @@ + + * @license LICENSE.txt + * + * @package QuickBooks + */ + +/** + * Class QuickBooks_IPP_OAuth2 + * + * @author Evgeniy Bogdanov + */ +class QuickBooks_IPP_OAuth2 +{ + const METHOD_POST = 'POST'; + const METHOD_GET = 'GET'; + const METHOD_PUT = 'PUT'; + + const MODE_PRODUCTION = 'production'; + const MODE_DEVELOPMENT = 'development'; + + /** + * Client Id + * + * @var string + */ + protected $_client_id; + + /** + * Client secret + * + * @var string + */ + protected $_client_secret; + + /** + * Access token + * + * @var string + */ + protected $_access_token; + + /** + * Refresh token + * + * @var string + */ + protected $_refresh_token; + + /** + * Realm Id + * + * @var int + */ + protected $_realm_id; + + /** + * Access token expire date + * + * @var \DateTime + */ + protected $_access_token_expires_time; + + /** + * Refresh token expire time + * + * @var \DateTime + */ + protected $_refresh_token_expires_time; + + /** + * @var string + */ + protected $_interaction_mode; + + /** + * Create our OAuth2 Token instance + * + * @param string $oauth2_client_id + * @param string $oauth2_client_secret + * @param int $realm_id + * @param string $oauth2_access_token + * @param string $oauth2_refresh_token + */ + public function __construct($oauth2_client_id, $oauth2_client_secret, $realm_id = null, $oauth2_access_token = '', $oauth2_refresh_token = '') + { + $this->setClientId($oauth2_client_id); + $this->setClientSecret($oauth2_client_secret); + + $this->setAccessToken($oauth2_access_token); + $this->setRefreshToken($oauth2_refresh_token); + $this->setRealmId($realm_id); + + $this->_interaction_mode = self::MODE_DEVELOPMENT; + } + + /** + * Instantiate object from array with settings + * + * @param array $creds + * @return static + */ + public static function fromArray($creds) + { + $instance = new static($creds['oauth2_client_id'], $creds['oauth2_client_secret'], $creds['qb_realm'], $creds['oauth2_access_token'], $creds['oauth2_refresh_token']); + + return $instance; + } + + /** + * Sign an OAuth request and return header value with sign + * + * @param string $method + * @param string $url + * @param array $params + * + * @return string + */ + public function sign($method, $url, $params = array()) + { + return 'Bearer ' . $this->_access_token; + } + + /** + * Get ClientID + * + * @return string + */ + public function getClientId() + { + return $this->_client_id; + } + + /** + * Set ClientID + * + * @param string $client_id + */ + public function setClientId($client_id) + { + $this->_client_id = $client_id; + } + + /** + * Get ClientSecret + * + * @return string + */ + public function getClientSecret() + { + return $this->_client_secret; + } + + /** + * Set ClientSecret + * + * @param string $client_secret + */ + public function setClientSecret($client_secret) + { + $this->_client_secret = $client_secret; + } + + /** + * Get Access Token + * + * @return string + */ + public function getAccessToken() + { + return $this->_access_token; + } + + /** + * Set OAuth2 Access token + * + * @param string $access_token + */ + public function setAccessToken($access_token) + { + $this->_access_token = $access_token; + } + + /** + * Get Refresh Token + * + * @return string + */ + public function getRefreshToken() + { + return $this->_refresh_token; + } + + /** + * Set Refresh Token + * + * @param string $refresh_token + */ + public function setRefreshToken($refresh_token) + { + $this->_refresh_token = $refresh_token; + } + + /** + * Get interaction mode value (development/production) + * + * @return string + */ + public function getInteractionMode() + { + return $this->_interaction_mode; + } + + /** + * Set interaction mode + * + * @param string $interaction_mode + * + * @throws Exception + */ + public function setInteractionMode($interaction_mode) + { + $values = array( + self::MODE_PRODUCTION, + self::MODE_DEVELOPMENT + ); + + $interaction_mode = strtolower($interaction_mode); + + if (!in_array($interaction_mode, $values)) + { + $msg = 'Interaction mode %s is not correct. Should be one of [%s]'; + $msg = sprintf($msg, $interaction_mode, implode(', ', $values)); + + throw new Exception($msg); + } + + $this->_interaction_mode = $interaction_mode; + } + + /** + * Get Realm Id + * + * @return int + */ + public function getRealmId() + { + return $this->_realm_id; + } + + /** + * Set Realm Id + * + * @param int $realm_id + */ + public function setRealmId($realm_id) + { + $this->_realm_id = $realm_id; + } + + /** + * @return string + */ + public function getAccessTokenExpiresTime() + { + return $this->_access_token_expires_time->format('Y-m-d H:i:s'); + } + + /** + * @param DateTime|string $access_token_expires_time + */ + public function setAccessTokenExpiresTime($access_token_expires_time) + { + if (is_scalar($access_token_expires_time)) + { + $access_token_expires_time = new \DateTime($access_token_expires_time); + } + + $this->_access_token_expires_time = $access_token_expires_time; + } + + /** + * @return string + */ + public function getRefreshTokenExpiresTime() + { + return $this->_refresh_token_expires_time->format('Y-m-d H:i:s'); + } + + /** + * @param DateTime|string $refresh_token_expires_time + */ + public function setRefreshTokenExpiresTime($refresh_token_expires_time) + { + if (is_scalar($refresh_token_expires_time)) + { + $refresh_token_expires_time = new \DateTime($refresh_token_expires_time); + } + + $this->_refresh_token_expires_time = $refresh_token_expires_time; + } +} diff --git a/QuickBooks/IPP/OAuth2Helper.php b/QuickBooks/IPP/OAuth2Helper.php new file mode 100644 index 00000000..5f1fbcb0 --- /dev/null +++ b/QuickBooks/IPP/OAuth2Helper.php @@ -0,0 +1,330 @@ + + * @license LICENSE.txt + * + * @package QuickBooks + */ + +/** + * Utilities class (for masking and some other misc things) + */ +QuickBooks_Loader::load('/QuickBooks/Utilities.php'); + +/** + * HTTP connection class + */ +QuickBooks_Loader::load('/QuickBooks/HTTP.php'); + +/** + * Class QuickBooks_IPP_OAuth2Helper + * + * @author Evgeniy Bogdanov + */ +class QuickBooks_IPP_OAuth2Helper +{ + const METHOD_POST = 'POST'; + const METHOD_GET = 'GET'; + const METHOD_PUT = 'PUT'; + + const SCOPE_ACCOUNTING = 'com.intuit.quickbooks.accounting'; + const SCOPE_PAYMENTS = 'com.intuit.quickbooks.payment'; + + const URL_TOKEN_ENDPOINT = 'https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer'; + const URL_AUTHORIZATION_REQUEST = 'https://appcenter.intuit.com/connect/oauth2'; + const URL_REVOKE_TOKEN_ENDPOINT = 'https://developer.api.intuit.com/v2/oauth2/tokens/revoke'; + + /** + * @var QuickBooks_IPP_OAuth2 + */ + protected $_oauth2_token_instance; + + /** + * QuickBooks_IPP_OAuth2Helper constructor. + * + * @param QuickBooks_IPP_OAuth2 $oauth2_token + */ + public function __construct(QuickBooks_IPP_OAuth2 $oauth2_token) + { + $this->setOauth2TokenInstance($oauth2_token); + } + + /** + * First step of OAuth2 + * + * @param string $redirect_url URL there user will be landed after grant access to his data + * @param string|array $scope By default com.intuit.quickbooks.accounting + * @param string|array $additional_options additional options which will be passed in redirect URL as "state" option + * + * @return string + */ + public function getAuthorizationURL($redirect_url, $scope = self::SCOPE_ACCOUNTING, $additional_options = '') + { + if (is_array($additional_options)) + { + $additional_options = http_build_query($additional_options, null, ';'); + } + + $parameters = array( + 'response_type' => 'code', + 'client_id' => $this->_oauth2_token_instance->getClientId(), + 'scope' => $this->getScopeAsString($scope), + 'redirect_uri' => $redirect_url, + 'state' => $additional_options ? $additional_options : 'none' + ); + + $auth_request_url = self::URL_AUTHORIZATION_REQUEST; + $auth_request_url .= '?' . http_build_query($parameters, null, '&', PHP_QUERY_RFC1738); + + return $auth_request_url; + } + + /** + * Load token by code Intuit have sent to our redirect url. + * + * @param string $code + * @param string $redirect_url + * @param int $realm_id + * + * @return QuickBooks_IPP_OAuth2 + * @throws Exception + */ + public function loadTokenByCode($code, $redirect_url, $realm_id = null) + { + $headers = array( + 'Accept' => 'application/json', + 'Content-Type' => 'application/x-www-form-urlencoded', + 'Request-Id' => QuickBooks_Utilities::GUID(), + 'Authorization' => $this->generateAuthorizationHeader() + ); + + $request_parameters = array( + 'grant_type' => 'authorization_code', + 'code' => $code, + 'redirect_uri' => $redirect_url + ); + $body = http_build_query($request_parameters); + + $HTTP = new QuickBooks_HTTP(self::URL_TOKEN_ENDPOINT); + $HTTP->setHeaders($headers); + $HTTP->setRawBody($body); + $HTTP->verifyHost(false); + $HTTP->verifyPeer(false); + $HTTP->POST(); + + $token = $this->parseResponse($HTTP); + $this->setOauth2TokenInstance($token); + + if ($realm_id) + { + $token->setRealmId($realm_id); + } + + return $token; + } + + /** + * Generate Authorization header + * + * @return string + */ + protected function generateAuthorizationHeader() + { + $parts = array( + $this->_oauth2_token_instance->getClientId(), + $this->_oauth2_token_instance->getClientSecret() + ); + + $encodedString = base64_encode(implode(':', $parts)); + $encodedString = rtrim($encodedString, '='); + + return 'Basic ' . $encodedString; + } + + /** + * Get a new access token based on the refresh token + * + * @return QuickBooks_IPP_OAuth2 + * @throws Exception + */ + public function refreshToken() + { + $refresh_token = $this->_oauth2_token_instance->getRefreshToken(); + + $headers = array( + 'Accept' => 'application/json', + 'Content-Type' => 'application/x-www-form-urlencoded', + 'Connection' => 'close', + 'Authorization' => $this->generateAuthorizationHeader() + ); + + $parameters = array( + 'grant_type' => 'refresh_token', + 'refresh_token' => $refresh_token + ); + $body = http_build_query($parameters); + + $HTTP = new QuickBooks_HTTP(self::URL_TOKEN_ENDPOINT); + $HTTP->setHeaders($headers); + $HTTP->setRawBody($body); + $HTTP->verifyHost(false); + $HTTP->verifyPeer(false); + $HTTP->POST(); + + // Build token from response + $token = $this->parseResponse($HTTP); + $this->setOauth2TokenInstance($token); + + return $token; + } + + /** + * Revoke access token + * + * @return bool + */ + public function revokeToken() + { + $request_parameters = array( + 'token' => strval($this->_oauth2_token_instance->getRefreshToken()) + ); + $body = json_encode($request_parameters); + + $headers = array( + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'Authorization' => $this->generateAuthorizationHeader() + ); + + $HTTP = new QuickBooks_HTTP(self::URL_REVOKE_TOKEN_ENDPOINT); + $HTTP->setHeaders($headers); + $HTTP->setRawBody($body); + $HTTP->verifyHost(false); + $HTTP->verifyPeer(false); + $HTTP->POST(); + + /** + * @todo Check for success + */ + + return true; + } + + /** + * Convert user specified scope to required format + * + * @param array|string $scope + * @return string + */ + protected function getScopeAsString($scope) + { + // if we've spaces, commas and this is string - cast to array + if (is_string($scope)) + { + $scope = str_replace(' ', ',', $scope); + $scope = explode(',', $scope); + } + + foreach ($scope as $key => $value) + { + if (($value != self::SCOPE_ACCOUNTING) && ($value != self::SCOPE_PAYMENTS)) + { + unset($scope[$key]); + } + } + + return implode(' ', $scope); + } + + /** + * Parse the JSON Body to store the information to an OAuth 2 Access Token + * + * @param QuickBooks_HTTP $HTTP + * + * @return QuickBooks_IPP_OAuth2 + * + * @throws Exception + */ + private function parseResponse(QuickBooks_HTTP $HTTP) + { + $info = $HTTP->lastInfo(); + $response = $HTTP->lastResponse(); + + /** + * @kludge + * + * JSON_BIGINT_AS_STRING helps to solve issues with bigint failed decode + * this works for PHP 5.4+, but for old versions I do not have good enough fix for now. + * + * @author Evgeniy Bogdanov + */ + $result = version_compare(PHP_VERSION, '5.4.0', '>=') + ? json_decode($response, false, 512, JSON_BIGINT_AS_STRING) + : json_decode($response, false); + + $http_code = $info['http_code']; + + if ($http_code < 400) + { + $http_code = 0; + } + + if (json_last_error()) + { + throw new Exception('JSON error : ' . json_last_error_msg(), $http_code); + } + + if (property_exists($result, 'error')) + { + throw new Exception($result->error, $http_code); + } + + if ($http_code) + { + throw new Exception('Server returned ' . $http_code . ' HTTP error'); + } + + $expire_access_token_date = new \DateTime(); + $expire_access_token_date->modify($result->expires_in . ' seconds'); + + $expire_refresh_token_date = new \DateTime(); + $expire_refresh_token_date->modify($result->x_refresh_token_expires_in . ' seconds'); + + $instance = $this->_oauth2_token_instance; + + $oauth2_token = new QuickBooks_IPP_OAuth2($instance->getClientId(), $instance->getClientSecret(), $instance->getRealmId()); + + $oauth2_token->setAccessToken($result->access_token); + $oauth2_token->setAccessTokenExpiresTime($expire_access_token_date); + $oauth2_token->setRefreshToken($result->refresh_token); + $oauth2_token->setRefreshTokenExpiresTime($expire_refresh_token_date); + + return $oauth2_token; + } + + /** + * @return QuickBooks_IPP_OAuth2 + */ + public function getOauth2TokenInstance() + { + return $this->_oauth2_token_instance; + } + + /** + * @param QuickBooks_IPP_OAuth2 $oauth2_token_instance + */ + public function setOauth2TokenInstance(QuickBooks_IPP_OAuth2 $oauth2_token_instance) + { + $this->_oauth2_token_instance = $oauth2_token_instance; + } + +} \ No newline at end of file diff --git a/QuickBooks/Payments.php b/QuickBooks/Payments.php index a8bfefda..ca835d67 100755 --- a/QuickBooks/Payments.php +++ b/QuickBooks/Payments.php @@ -180,7 +180,7 @@ public function debit($Context, $Object_or_token, $amount, $description = '') * * See also: * https://developer.intuit.com/docs/api/payments/charges - * + * * @param [type] $Context [description] * @param [type] $Object_or_token [description] * @param [type] $amount [description] @@ -196,11 +196,11 @@ public function charge($Context, $Object_or_token, $amount, $currency = 'USD', $ } /** - * Authorize (but don't do a full charge/capture) a credit card + * Authorize (but don't do a full charge/capture) a credit card * * See also: * https://developer.intuit.com/docs/api/payments/charges - * + * * @param [type] $Context [description] * @param [type] $Object_or_token [description] * @param [type] $amount [description] @@ -221,9 +221,9 @@ public function _chargeOrAuth($Context, $Object_or_token, $amount, $currency, $c 'amount' => sprintf('%01.2f', $amount), 'currency' => $currency, 'context' => array( - 'mobile' => false, - 'isEcommerce' => false, - 'recurring' => false, + 'mobile' => false, + 'isEcommerce' => false, + 'recurring' => false, ) ); @@ -255,6 +255,15 @@ public function _chargeOrAuth($Context, $Object_or_token, $amount, $currency, $c $data = json_decode($resp, true); + if (empty($data)) + { + // If we didn't get anything back at all, it could be an HTTP + // time-out which we will report as failure + + $this->_setError(self::ERROR_HTTP, 'Communication error while processing request.'); + return false; + } + if ($this->_handleError($data)) { return false; @@ -341,8 +350,8 @@ public function getDebit($Context, $id) } /** - * Refund a transaction - * + * Refund a transaction + * * @param [type] $Context [description] * @param [type] $id [description] * @param [type] $amount [description] @@ -356,9 +365,9 @@ public function refund($Context, $id, $amount, $context = array()) $payload = array( 'amount' => $amount, 'context' => array( - 'mobile' => false, - 'isEcommerce' => false, - 'recurring' => false, + 'mobile' => false, + 'isEcommerce' => false, + 'recurring' => false, ), ); @@ -538,6 +547,16 @@ protected function _handleError($data, $ignore_declines = false) $this->_setError($info['http_code'], 'Unauthorized.'); return true; } + else if ($info['http_code'] == QuickBooks_HTTP::HTTP_404) + { + $this->_setError($info['http_code'], 'Not Found.'); + return true; + } + else if ($info['http_code'] == QuickBooks_HTTP::HTTP_500) + { + $this->_setError($info['http_code'], 'Internal Server Error.'); + return true; + } } if (isset($data['errors'])) diff --git a/QuickBooks/Utilities.php b/QuickBooks/Utilities.php index b2a45523..9d6bf36f 100755 --- a/QuickBooks/Utilities.php +++ b/QuickBooks/Utilities.php @@ -695,8 +695,6 @@ static public function fnmatch($pattern, $str) /** * List all of the QuickBooks object types supported by the framework * - * @todo We might be able to optimize this a bit to not use create_function() - * * @param string $filter * @param boolean $return_keys * @param boolean $order_for_mapping @@ -742,9 +740,7 @@ static public function listObjects($filter = null, $return_keys = false, $order_ if ($order_for_mapping) { // Sort with the very longest values first, to the shortest values last - - $func = create_function('$a, $b', ' if (strlen($a) > strlen($b)) { return -1; } return 1; '); - usort($constants, $func); + usort($constants, function($a, $b){ return strlen($a) > strlen($b) ? -1 : 1; }); } else { diff --git a/docs/partner_platform/example_app_ipp_v3/config.php b/docs/partner_platform/example_app_ipp_v3/config.php index 111cea5e..3c9f458c 100755 --- a/docs/partner_platform/example_app_ipp_v3/config.php +++ b/docs/partner_platform/example_app_ipp_v3/config.php @@ -26,10 +26,16 @@ // store them somewhere safe. // // The OAuth request/access tokens will be encrypted and stored for you by the -// PHP DevKit IntuitAnywhere classes automatically. +// PHP DevKit IntuitAnywhere classes automatically. +// NOTE: OAuth1 is deprecated method. Applications registered after 17th July 2017 should use OAuth2 $oauth_consumer_key = 'qyprdfkqo3bikN2vLrLu4FWHv6GbQp'; $oauth_consumer_secret = 'WDH56afDb1jr0ismQZAwdPuq4oDqpTmrKXc0oORz'; +// OAuth2 is method for all applications registered after 17th July 2017. +// You may want to change those Ids +$oauth2_client_id = 'Q0yebRJgPF0R5DqdZVzOpeAe4B1pFgdmBSaFcD0eVLHDMDH7r9'; +$oauth2_client_secret = 'OInt0KjDeruNFAnah0kPzkwzmtElo36FvhBUSUv2'; + // If you're using DEVELOPMENT TOKENS, you MUST USE SANDBOX MODE!!! If you're in PRODUCTION, then DO NOT use sandbox. $sandbox = true; // When you're using development tokens //$sandbox = false; // When you're using production tokens @@ -72,16 +78,36 @@ // $oauth_consumer_secret Intuit will give this to you too // $this_url This is the full URL (e.g. http://path/to/this/file.php) of THIS SCRIPT // $that_url After the user authenticates, they will be forwarded to this URL -// -$IntuitAnywhere = new QuickBooks_IPP_IntuitAnywhere($dsn, $encryption_key, $oauth_consumer_key, $oauth_consumer_secret, $quickbooks_oauth_url, $quickbooks_success_url); +// +$oauth_version = QuickBooks_IPP::AUTHMODE_OAUTH; +if (empty($oauth2_client_id)) +{ + $IntuitAnywhere = new QuickBooks_IPP_IntuitAnywhere($dsn, $encryption_key, $oauth_consumer_key, $oauth_consumer_secret, $quickbooks_oauth_url, $quickbooks_success_url); +} +else +{ + $oauth_version = QuickBooks_IPP::AUTHMODE_OAUTH2; + + $token = new QuickBooks_IPP_OAuth2($oauth2_client_id, $oauth2_client_secret); + $helper = new QuickBooks_IPP_OAuth2Helper($token); + + $IntuitAnywhere = new QuickBooks_IPP_IntuitAnywhereOAuth2($dsn, $encryption_key, $oauth2_client_id, $oauth2_client_secret, $quickbooks_oauth_url, $quickbooks_success_url); + // Refresh token if expired + $IntuitAnywhere->refresh_expired_token($the_username, $the_tenant); + + if ($IntuitAnywhere->errorNumber()) + { + echo 'Unable to refresh access token: ' . $IntuitAnywhere->errorMessage(); + } + + $quickbooks_oauth_url = $helper->getAuthorizationURL($quickbooks_oauth_url); +} + +$quickbooks_is_connected = false; // Are they connected to QuickBooks right now? -if ($IntuitAnywhere->check($the_username, $the_tenant) and - $IntuitAnywhere->test($the_username, $the_tenant)) +if ($IntuitAnywhere->check($the_username, $the_tenant)) { - // Yes, they are - $quickbooks_is_connected = true; - // Set up the IPP instance $IPP = new QuickBooks_IPP($dsn); @@ -90,7 +116,7 @@ // Tell the framework to load some data from the OAuth store $IPP->authMode( - QuickBooks_IPP::AUTHMODE_OAUTH, + $oauth_version, $the_username, $creds); @@ -112,9 +138,10 @@ // Get some company info $CompanyInfoService = new QuickBooks_IPP_Service_CompanyInfo(); $quickbooks_CompanyInfo = $CompanyInfoService->get($Context, $realm); -} -else -{ - // No, they are not - $quickbooks_is_connected = false; + + if (!empty($quickbooks_CompanyInfo)) + { + // Yes, they are + $quickbooks_is_connected = true; + } } \ No newline at end of file diff --git a/docs/web_connector/example_web_connector_import.php b/docs/web_connector/example_web_connector_import.php index 7d9a89e9..b7f5e7fc 100755 --- a/docs/web_connector/example_web_connector_import.php +++ b/docs/web_connector/example_web_connector_import.php @@ -250,11 +250,11 @@ function _quickbooks_hook_loginsuccess($requestID, $user, $hook, &$err, $hook_da } // Make sure the requests get queued up - //$Queue->enqueue(QUICKBOOKS_IMPORT_SALESORDER, 1, QB_PRIORITY_SALESORDER, $user); - //$Queue->enqueue(QUICKBOOKS_IMPORT_INVOICE, 1, QB_PRIORITY_INVOICE, $user); - $Queue->enqueue(QUICKBOOKS_IMPORT_PURCHASEORDER, 1, QB_PRIORITY_PURCHASEORDER, $user); - $Queue->enqueue(QUICKBOOKS_IMPORT_CUSTOMER, 1, QB_PRIORITY_CUSTOMER, $user); - //$Queue->enqueue(QUICKBOOKS_IMPORT_ITEM, 1, QB_PRIORITY_ITEM, $user); + //$Queue->enqueue(QUICKBOOKS_IMPORT_SALESORDER, 1, QB_PRIORITY_SALESORDER, null, $user); + //$Queue->enqueue(QUICKBOOKS_IMPORT_INVOICE, 1, QB_PRIORITY_INVOICE, null, $user); + $Queue->enqueue(QUICKBOOKS_IMPORT_PURCHASEORDER, 1, QB_PRIORITY_PURCHASEORDER, null, $user); + $Queue->enqueue(QUICKBOOKS_IMPORT_CUSTOMER, 1, QB_PRIORITY_CUSTOMER, null, $user); + //$Queue->enqueue(QUICKBOOKS_IMPORT_ITEM, 1, QB_PRIORITY_ITEM, null, $user); } /** @@ -370,7 +370,7 @@ function _quickbooks_invoice_import_response($requestID, $user, $action, $ID, $e // Queue up another request $Queue = QuickBooks_WebConnector_Queue_Singleton::getInstance(); - $Queue->enqueue(QUICKBOOKS_IMPORT_INVOICE, null, QB_PRIORITY_INVOICE, array( 'iteratorID' => $idents['iteratorID'] )); + $Queue->enqueue(QUICKBOOKS_IMPORT_INVOICE, null, QB_PRIORITY_INVOICE, array( 'iteratorID' => $idents['iteratorID'] ), $user); } // This piece of the response from QuickBooks is now stored in $xml. You @@ -518,7 +518,7 @@ function _quickbooks_customer_import_response($requestID, $user, $action, $ID, $ // Queue up another request $Queue = QuickBooks_WebConnector_Queue_Singleton::getInstance(); - $Queue->enqueue(QUICKBOOKS_IMPORT_CUSTOMER, null, QB_PRIORITY_CUSTOMER, array( 'iteratorID' => $idents['iteratorID'] )); + $Queue->enqueue(QUICKBOOKS_IMPORT_CUSTOMER, null, QB_PRIORITY_CUSTOMER, array( 'iteratorID' => $idents['iteratorID'] ), $user); } // This piece of the response from QuickBooks is now stored in $xml. You @@ -634,7 +634,7 @@ function _quickbooks_salesorder_import_response($requestID, $user, $action, $ID, // Queue up another request $Queue = QuickBooks_WebConnector_Queue_Singleton::getInstance(); - $Queue->enqueue(QUICKBOOKS_IMPORT_SALESORDER, null, QB_PRIORITY_SALESORDER, array( 'iteratorID' => $idents['iteratorID'] )); + $Queue->enqueue(QUICKBOOKS_IMPORT_SALESORDER, null, QB_PRIORITY_SALESORDER, array( 'iteratorID' => $idents['iteratorID'] ), $user); } // This piece of the response from QuickBooks is now stored in $xml. You @@ -782,7 +782,7 @@ function _quickbooks_item_import_response($requestID, $user, $action, $ID, $extr // Queue up another request $Queue = QuickBooks_WebConnector_Queue_Singleton::getInstance(); - $Queue->enqueue(QUICKBOOKS_IMPORT_ITEM, null, QB_PRIORITY_ITEM, array( 'iteratorID' => $idents['iteratorID'] )); + $Queue->enqueue(QUICKBOOKS_IMPORT_ITEM, null, QB_PRIORITY_ITEM, array( 'iteratorID' => $idents['iteratorID'] ), $user); } // Import all of the records @@ -922,7 +922,7 @@ function _quickbooks_purchaseorder_import_response($requestID, $user, $action, $ // Queue up another request $Queue = QuickBooks_WebConnector_Queue_Singleton::getInstance(); - $Queue->enqueue(QUICKBOOKS_IMPORT_PURCHASEORDER, null, QB_PRIORITY_PURCHASEORDER, array( 'iteratorID' => $idents['iteratorID'] )); + $Queue->enqueue(QUICKBOOKS_IMPORT_PURCHASEORDER, null, QB_PRIORITY_PURCHASEORDER, array( 'iteratorID' => $idents['iteratorID'] ), $user); } // This piece of the response from QuickBooks is now stored in $xml. You