Skip to content

Instantly share code, notes, and snippets.

@mjb
Last active March 15, 2021 22:38
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save mjb/8567585 to your computer and use it in GitHub Desktop.
Save mjb/8567585 to your computer and use it in GitHub Desktop.
Using CFML (Railo & ColdFusion) to access the the Xero API Private Application.
<!-----------------------------------------------------------------------------------
@@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 sharadg@gmail.com (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>
@andybellenie
Copy link

Thanks for publishing this, really helped me with a project.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment