-
-
Save dawesi/75a699426cb1fd6ed5ad to your computer and use it in GitHub Desktop.
Using CFML (Railo & ColdFusion) to access the the Xero API Private Application.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!----------------------------------------------------------------------------------- | |
@@Author: Matthew Bryant | |
@@Company: TORO Waste Equipment | |
@@license: The MIT License (http://opensource.org/licenses/MIT) | |
@@Version: 0.1 $ | |
@@Description: For use with Xero API (https://api.xero.com) | |
STEP 1. GENERATE SSL KEY | |
1.1. Download openSSL for windows: http://slproweb.com/products/Win32OpenSSL.html | |
1.2. Create SSL Certificate | |
1.2.1. openssl genrsa -out xero_privatekey.pem 1024 | |
1.2.2. openssl req -newkey rsa:1024 -x509 -key xero_privatekey.pem -out xero_publickey.cer -days 365 | |
---- The .cer will be needed when creating the API application in the Xero Developer Centre ‘Add Application’ screen | |
1.2.3. openssl pkcs8 -topk8 -nocrypt -in xero_privatekey.pem -out xero_privatekey.pcks8 | |
---- Extracts the private key in PKCS8 format used for signature in this cfc: | |
STEP 2. CREATE XERO APPLICATION FROM DEVELOPER CENTER | |
2.1. Use public key (.cer) generated above to create API Application to link to your xero company file here: https://api.xero.com/Application | |
STEP 3. EXAMPLE USAGE: | |
<cfsavecontent variable="xeroXML"> | |
<cfoutput> | |
<Contacts> | |
<Contact> | |
<Name>ABC Limited</Name> | |
</Contact> | |
<Contact> | |
<Name>DEF Limited</Name> | |
</Contact> | |
</Contacts> | |
</cfoutput> | |
</cfsavecontent> | |
<cfset oXero = createObject("component", "path.to.this.file").init( privatePCKS8KeyPath="/path/to/xero_privatekey.pcks8", | |
consumerKey="AABBCCDDEEFFGGHHIIJJKKLLMMNNOO") /> | |
<cfset stResult = oXero.sendRequest( method="POST", | |
endPoint="Contacts", | |
xml="#xeroXML#", | |
SummarizeErrors="false") > | |
<cfdump var="#stResult#" /> | |
STEP 4. FURTHER READING. INFO THAT HELPED ME OUT. | |
---- https://dev.twitter.com/docs/auth/creating-signature | |
---- http://www.delbridge.org/post.cfm/tackling-twitter-s-oauth-with-coldfusion | |
---- http://quonos.nl/oauthTester/ | |
---- http://oauth.googlecode.com/svn/code/javascript/example/signature.html | |
-----------------------------------------------------------------------------------> | |
<cfcomponent displayname="XERO API" | |
hint="Rudimentary API for all things XERO" | |
output="false" | |
bDocument="true" | |
scopelocation="application.xero"> | |
<cffunction name="init"> | |
<cfargument name="privatePCKS8KeyPath" hint="Full path to pkcs8 format of your ssl key."> | |
<cfargument name="consumerKey" hint="The consumer key "> | |
<cfset variables.consumerKey = arguments.consumerKey> | |
<cfset variables.apiURL = "https://api.xero.com/api.xro/2.0"> | |
<cffile action="read" file="#arguments.privatePCKS8KeyPath#" variable="variables.privatePCKS8Key"> | |
<cfset variables.privatePCKS8Key = replaceNoCase(variables.privatePCKS8Key,"-----BEGIN PRIVATE KEY-----#chr(10)#","","All")> | |
<cfset variables.privatePCKS8Key = replaceNoCase(variables.privatePCKS8Key,"-----END PRIVATE KEY-----","","All")> | |
<cfset variables.privatePCKS8Key = trim(variables.privatePCKS8Key)> | |
<cfreturn this> | |
</cffunction> | |
<cffunction name="sendRequest" access="public"> | |
<cfargument name="method" type="string" hint="POST,PUT,GET" /> | |
<cfargument name="endPoint" type="string" hint="Invoices, Contacts, Items etc..." /> | |
<cfargument name="xml" type="string" default="" hint="Valid xml for the endpoint above." /> | |
<cfargument name="IfModifiedSince" type="string" default="" hint="The easiest way to retrieve resources that have been created or modified since a previous request is to specify a UTC timestamp filter using the If-Modified-Since http parameter. Only items created or updated since the specified timestamp will be returned." /> | |
<cfargument name="SummarizeErrors" type="boolean" default="true" hint="It is possible to submit more than one invoice, credit note, contact, item or other entities of the same type in a single API call. If you plan to submit more than one entity per API call, we recommend that you use summarizeErrors=false" /> | |
<cfset var Math = createObject('java','java.lang.Math') /> | |
<cfset var randNum = createObject('java', 'java.security.SecureRandom') /> | |
<cfset var numeric_nonce = Math.abs(JavaCast("long",randNum.nextLong())) /> | |
<cfset var nonce = numeric_nonce.toString() /> | |
<cfset var timestmp = DateDiff("s",DateConvert("utc2local", "January 1 1970 00:00"), Now()) /> | |
<cfset var httpURL = "#variables.apiURL#/#arguments.endPoint#"> | |
<cfset var signatureString = ""> | |
<cfset var signatureStringURL = "#httpURL#"> | |
<cfset var signatureStringParameters = "oauth_consumer_key=#variables.consumerKey#&oauth_nonce=#nonce#&oauth_signature_method=RSA-SHA1&oauth_timestamp=#timestmp#&oauth_token=#variables.consumerKey#&oauth_version=1.0"> | |
<cfset var signatureXML = urlEncoder(arguments.xml)> | |
<cfset var appSignature = ""> | |
<cfset var stResult = ""> | |
<cfif NOT arguments.SummarizeErrors> | |
<cfset signatureStringParameters = "#signatureStringParameters#&summarizeErrors=false" /> | |
</cfif> | |
<cfif listFindNoCase("POST,PUT",arguments.method)> | |
<cfset signatureStringParameters = "#signatureStringParameters#&xml=#signatureXML#" /> | |
</cfif> | |
<cfset signatureString = "#uCase(arguments.method)#&#urlEncoder(signatureStringURL)#&#urlEncoder(signatureStringParameters)#"> | |
<cfset appSignature = rsa_sha1(signKey="#variables.privatePCKS8Key#", signMessage='#trim(signatureString)#') /> | |
<cfhttp url="#httpURL#" method="#arguments.method#" result="stResult" > | |
<cfif listFindNoCase("POST,PUT",arguments.method)> | |
<cfhttpparam type="formfield" name="oauth_consumer_key" value="#variables.consumerKey#" /> | |
<cfhttpparam type="formfield" name="oauth_nonce" value="#nonce#" /> | |
<cfhttpparam type="formfield" name="oauth_signature" value="#appSignature#" /> | |
<cfhttpparam type="formfield" name="oauth_signature_method" value="RSA-SHA1" /> | |
<cfhttpparam type="formfield" name="oauth_timestamp" value="#timestmp#" /> | |
<cfhttpparam type="formfield" name="oauth_token" value="#variables.consumerKey#" /> | |
<cfhttpparam type="formfield" name="oauth_version" value="1.0" /> | |
<cfif NOT arguments.SummarizeErrors> | |
<cfhttpparam type="formfield" name="summarizeErrors" value="false" /> | |
</cfif> | |
<cfif len(arguments.xml)> | |
<cfhttpparam type="formfield" name="xml" value="#trim(arguments.xml)#" /> | |
</cfif> | |
<cfelse> | |
<cfhttpparam type="url" name="oauth_consumer_key" value="#variables.consumerKey#" /> | |
<cfhttpparam type="url" name="oauth_nonce" value="#nonce#" /> | |
<cfhttpparam type="url" name="oauth_signature" value="#appSignature#" /> | |
<cfhttpparam type="url" name="oauth_signature_method" value="RSA-SHA1" /> | |
<cfhttpparam type="url" name="oauth_timestamp" value="#timestmp#" /> | |
<cfhttpparam type="url" name="oauth_token" value="#variables.consumerKey#" /> | |
<cfhttpparam type="url" name="oauth_version" value="1.0" /> | |
<cfif NOT arguments.SummarizeErrors> | |
<cfhttpparam type="url" name="summarizeErrors" value="false" /> | |
</cfif> | |
</cfif> | |
<cfif arguments.method EQ "GET" and isDate(arguments.IfModifiedSince)> | |
<cfset arguments.IfModifiedSince = DateConvert("local2utc", arguments.IfModifiedSince) > | |
<cfhttpparam type="header" name="If-Modified-Since" value="#dateFormat(arguments.IfModifiedSince,'yyyy-mm-dd')#T#timeFormat(arguments.IfModifiedSince,'HH:mm:ss')#" /> | |
</cfif> | |
</cfhttp> | |
<cfif isXML(stResult.Filecontent)> | |
<cftry> | |
<cfreturn ConvertXmlToStruct(stResult.Filecontent,structNew())> | |
<cfcatch type="any"> | |
<cfdump var="#cfcatch#" expand="false"> | |
<cfreturn stResult> | |
</cfcatch> | |
</cftry> | |
<cfelse> | |
<cfreturn stResult> | |
</cfif> | |
</cffunction> | |
<!--- ************************************************************ ---> | |
<!--- RFC 3986-COMPLIANT URLENCODEDFORMAT() FUNCTION ---> | |
<!--- ---> | |
<!--- Per "URL Encoding to RFC 3986" in Adobe's Developer ---> | |
<!--- Connection, this function corrects inconsistencies in ---> | |
<!--- ColdFusion's URLEncodedFormat() function that are known ---> | |
<!--- to break OAuth authentication attempts. ---> | |
<!--- ---> | |
<!--- AUTHOR ---> | |
<!--- Dave Delbridge, Circa 3000 (http://circa3000.com) ---> | |
<!--- ---> | |
<!--- PARAMETERS ---> | |
<!--- URL (string) = address to be url-encoded ---> | |
<!--- ---> | |
<!--- RETURNS ---> | |
<!--- (string) Url-encoded address, per RFC 3986 ---> | |
<!--- ---> | |
<!--- ************************************************************ ---> | |
<!--- ************************************************************ ---> | |
<!--- Perform URL encoding and correct mistakes ---> | |
<!--- ************************************************************ ---> | |
<cffunction name="urlEncoder" returntype="string" access="public" output="no" hint="ColdFusion default urlEncode does not encode in the required format."> | |
<cfargument name="url" type="string" required="true" /> | |
<cfset var rfc_3986_bad_chars = "%2D,%2E,%5F,%7E"> | |
<cfset var rfc_3986_good_chars = "-,.,_,~"> | |
<cfset arguments.url = ReplaceList(URLEncodedFormat(trim(arguments.url)),rfc_3986_bad_chars,rfc_3986_good_chars)> | |
<cfreturn arguments.url /> | |
</cffunction> | |
<cffunction name="rsa_sha1" returntype="string" access="private" descrition="RSA-SHA1 computation based on supplied private key and supplied base signature string."> | |
<!---Written by Sharad Gupta [email protected] (used with permission)---> | |
<cfargument name="signKey" type="string" required="true" hint="base64 formatted PKCS8 private key"> | |
<cfargument name="signMessage" type="string" required="true" hint="msg to sign"> | |
<cfargument name="sFormat" type="string" required="false" default="UTF-8"> | |
<cfset var jKey = JavaCast("string", trim(arguments.signKey))> | |
<cfset var jMsg = JavaCast("string",arguments.signMessage).getBytes(arguments.sFormat)> | |
<cfset var key = createObject("java", "java.security.PrivateKey")> | |
<cfset var keySpec = createObject("java","java.security.spec.PKCS8EncodedKeySpec")><!--- PKCS8EncodedKeySpec ---> | |
<cfset var keyFactory = createObject("java","java.security.KeyFactory")> | |
<cfset var b64dec = createObject("java", "sun.misc.BASE64Decoder")> | |
<cfset var sig = createObject("java", "java.security.Signature")> | |
<cfset var byteClass = createObject("java", "java.lang.Class")> | |
<cfset var byteArray = createObject("java","java.lang.reflect.Array")> | |
<cfset byteClass = byteClass.forName(JavaCast("string","java.lang.Byte"))> | |
<cfset keyBytes = byteArray.newInstance(byteClass, JavaCast("int","1024"))> | |
<cfset keyBytes = b64dec.decodeBuffer(jKey)> | |
<cfset sig = sig.getInstance("SHA1withRSA", "SunJSSE")> | |
<cfset sig.initSign(keyFactory.getInstance("RSA").generatePrivate(keySpec.init(keyBytes)))> | |
<cfset sig.update(jMsg)> | |
<cfset signBytes = sig.sign()> | |
<cfreturn ToBase64(signBytes)> | |
</cffunction> | |
<cffunction name="ConvertXmlToStruct" access="private" returntype="struct" output="true" | |
hint="Parse raw XML response body into ColdFusion structs and arrays and return it."> | |
<cfargument name="xmlNode" type="string" required="true" /> | |
<cfargument name="str" type="struct" required="true" /> | |
<!---Setup local variables for recurse: ---> | |
<cfset var i = 0 /> | |
<cfset var axml = arguments.xmlNode /> | |
<cfset var astr = arguments.str /> | |
<cfset var n = "" /> | |
<cfset var tmpContainer = "" /> | |
<cftry> | |
<!--- | |
Strip out the tag prefixes. This will convert tags from the | |
form of soap:nodeName to JUST nodeName. This works for both | |
openning and closing tags. | |
---> | |
<cfset arguments.xmlNode = arguments.xmlNode.ReplaceAll( | |
"(</?)(\w+:)", | |
"$1" | |
) /> | |
<!--- | |
Remove all references to XML name spaces. These are node | |
attributes that begin with "xmlns:". | |
---> | |
<cfset arguments.xmlNode = arguments.xmlNode.ReplaceAll( | |
"xmlns(:\w+)?=""[^""]*""", | |
"" | |
) /> | |
<!--- | |
Remove all references to XML name spaces. These are node | |
attributes that begin with "xsi:". | |
---> | |
<cfset arguments.xmlNode = arguments.xmlNode.ReplaceAll( | |
"xsi(:\w+)?=""[^""]*""", | |
"" | |
) /> | |
<cfcatch type="any"><!--- IGNORE ERRORS. JUST TRYING TO STRIP NAMESPACES WHERE APPROPRIATE ---></cfcatch> | |
</cftry> | |
<cfset axml = XmlSearch(XmlParse(arguments.xmlNode),"/node()")> | |
<cfset axml = axml[1] /> | |
<!--- For each children of context node: ---> | |
<cfloop from="1" to="#arrayLen(axml.XmlChildren)#" index="i"> | |
<!--- Read XML node name without namespace: ---> | |
<cfset n = replace(axml.XmlChildren[i].XmlName, axml.XmlChildren[i].XmlNsPrefix&":", "") /> | |
<!--- If key with that name exists within output struct ... ---> | |
<cfif structKeyExists(astr, n)> | |
<!--- ... and is not an array... ---> | |
<cfif not isArray(astr[n])> | |
<!--- ... get this item into temp variable, ... ---> | |
<cfset tmpContainer = astr[n] /> | |
<!--- ... setup array for this item beacuse we have multiple items with same name, ... ---> | |
<cfset astr[n] = arrayNew(1) /> | |
<!--- ... and reassing temp item as a first element of new array: ---> | |
<cfset astr[n][1] = tmpContainer /> | |
<cfelse> | |
<!--- Item is already an array: ---> | |
</cfif> | |
<cfif arrayLen(axml.XmlChildren[i].XmlChildren) gt 0> | |
<!--- recurse call: get complex item: ---> | |
<cfset astr[n][arrayLen(astr[n])+1] = ConvertXmlToStruct(axml.XmlChildren[i], structNew()) /> | |
<cfelse> | |
<!--- else: assign node value as last element of array: ---> | |
<cfset astr[n][arrayLen(astr[n])+1] = axml.XmlChildren[i].XmlText /> | |
</cfif> | |
<cfelse> | |
<!--- | |
This is not a struct. This may be first tag with some name. | |
This may also be one and only tag with this name. | |
---> | |
<!--- | |
If context child node has child nodes (which means it will be complex type): ---> | |
<cfif arrayLen(axml.XmlChildren[i].XmlChildren) gt 0> | |
<!--- recurse call: get complex item: ---> | |
<cfset astr[n] = ConvertXmlToStruct(axml.XmlChildren[i], structNew()) /> | |
<cfelse> | |
<!--- else: assign node value as last element of array: ---> | |
<!--- if there are any attributes on this element---> | |
<cfif IsStruct(aXml.XmlChildren[i].XmlAttributes) AND StructCount(aXml.XmlChildren[i].XmlAttributes) GT 0> | |
<!--- assign the text ---> | |
<cfset astr[n] = axml.XmlChildren[i].XmlText /> | |
<!--- check if there are no attributes with xmlns: , we dont want namespaces to be in the response---> | |
<cfset attrib_list = StructKeylist(axml.XmlChildren[i].XmlAttributes) /> | |
<cfloop from="1" to="#listLen(attrib_list)#" index="attrib"> | |
<cfif ListgetAt(attrib_list,attrib) CONTAINS "xmlns:"> | |
<!--- remove any namespace attributes---> | |
<cfset Structdelete(axml.XmlChildren[i].XmlAttributes, listgetAt(attrib_list,attrib))> | |
</cfif> | |
</cfloop> | |
<!--- if there are any atributes left, append them to the response---> | |
<cfif StructCount(axml.XmlChildren[i].XmlAttributes) GT 0> | |
<cfset astr[n&'_attributes'] = axml.XmlChildren[i].XmlAttributes /> | |
</cfif> | |
<cfelse> | |
<cfset astr[n] = axml.XmlChildren[i].XmlText /> | |
</cfif> | |
</cfif> | |
</cfif> | |
</cfloop> | |
<!--- return struct: ---> | |
<cfreturn astr /> | |
</cffunction> | |
</cfcomponent> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment