Common programming mistakes for ColdFusion programmer to avoid? [closed] - coldfusion

As it currently stands, this question is not a good fit for our Q&A format. We expect answers to be supported by facts, references, or expertise, but this question will likely solicit debate, arguments, polling, or extended discussion. If you feel that this question can be improved and possibly reopened, visit the help center for guidance.
Closed 11 years ago.
In the spirit of my other questions regarding "common programming mistakes ... to avoid"
What are some common programming mistakes for a ColdFusion programmer to avoid?

set <cffile> upload path to a web accessible, CF-enabled directory!!!
isStruct() before isObject() in a series of <cfif>'s expecting isStruct only catches struct (cfc component returns True from isStruct() as well)
no HtmlEditFormat() when displaying user-generated content (XSS)
forgot to add output=false on CFC methods
not using <cfqueryparam> inside <cfquery>
not scoping not-so-evident variables like cfquery name or loop index in a method
use <cfform> when all they need is plain-vanilla HTML <form>
forgot to UrlEncodedFormat() user-defined URL
use <cffeed> without sanitizing the content
trust isDate() too much (any number would return true)
expect string comparison to be case-sensitive (IS and EQ operators are case-insensitive)
sending strings "yes" or "no" to SerializeJSON() without appending a whitespace to preserve the string (otherwise SerializeJSON() or DeserializeJSON() will translate them to "true" and "false")
not putting singletons services in application scope
blindly create as much CFCs as one wants like one would do in JAVA
putting complex value/object into a list (can't, list is just a string of comma-seperated values)
writing functions that takes array as an argument and modify that array expecting that array will be modified (array in CFML is passed by value)
blindly changes access="remote" on a method and expect it to work (when remote proxy is generally more appropriate)
use a lot of WriteOutput() in cfscript when CFML is more appropriate
blindly uses IsDefined() when StructKeyExists() can generally do it more efficiently
blindly uses Iif() and De() without knowing they're as nasty as Evaluate()
update some code in onApplicationStart() and not seeing the difference on refresh (restart the app!)
<cfloop> or '' outside of <cfquery> causing multiple new query connections to be opened. 99% of the time it's better to have multiple statements inside of one cfquery to perform multiple actions, or to UNION data together.
hardcoding absolute path when ExpandPath() is generally better
forgot to turn on Unicode support in DSN (Unicode becomes '????')
not upgrading to the latest JRE and Hotfixes
misusing Client scope and blow up Windows registry...
uses depreciated/obsolete functions/features (i.e. flash form aka flex 1.x alpha, cftable, Verity full-text search, etc...)
passing CFCATCH to a function as argument type Struct (CFCATCH behaves like a Struct, but it is not. Just pass it as type 'Any').
Not reading CFC Best Practices from ColdBox wiki.
buying in the mindset of .ASP(X) or .JSP or [insert web technology] are always better.. ;)
not use PrecisionEvaluate() and getting all sort of floating point rounding error especially when calculating money.

Inappropriate use of #
SELECT *
Not scrubbing URL/form inputs
Debugging on in production environment (even if output is suppressed)

Shamelessly stealing Henry's formatting...
it is faster and more accurate to check explicit boolean instead of implied; use <cfif query.recordCount GT 0> instead of <cfif query.recordCount>
do not use evaluate(), de(), or iif()... ever. there is always a way around these slow functions
understand structures, keys, values and how to access query and structure data using array notation. (this will usually get around your need for evaluate())
do not use pound signs unless you are outputting data or otherwise creating a string (don't do this: myFunction(arg = #myVar#) )
read and understand the difference between THIS and VARIABLES scope in a CFC
avoid extreme overuse of <cfsilent> when you probably need to use a <cfcontent reset="true"> just before you begin your output (before doctype, xml declaration, or <html>)
don't blindly drop ColdFusion values into an HTML script block (javascript) without using jsStringFormat()
if you are not using <CDATA> text in your XML, you may want to use xmlFormat() when creating an XML document
don't use Windows registry for Client scope data. Use the database.
if your IT architecture permits, use Session data instead of Client data.
use <cflock> correctly and consistently; shared data will leak in your Application.
if you are going to use Java objects, understand the Java error messages (for example, 'method cannot be found' may not mean the method does not exist at all, it means the method does not exist for the arguments you've supplied)
if you have to read large files, either use the new CF8 "File" functions or hand off the task to Java on CF6 & 7. <cffile> is inefficient for large files.
understand pass-by-reference and pass-by-value, and how those concepts work in CF; specifically when using functions to modify XML documents
as Henry stated, always use <cfqueryparam>; also ensure that you are using the correct CFSQLType parameter for your DBMS (for date, time, timestamp, etc)
don't chain together a series of <cfif> and <cfelseif> logic blocks, use <cfswitch> and <cfcase> if you have more than three conditions you need to handle
more of an architecture note: always do some sort of server side validation to catch the nasty data the wolf-shirt-wearing user may be passing you
last architecture note: let CF do your middle layer of data retrieval and display and let your webserver do webserver things like SEO URLs (I'm looking at you ColdCourse)

SQL Injection Attacks. It seems like cfquery is just made to allow them. So you should use cfqueryparams.

In Coldfusion, all variables are global by default, unless they are declared with the var keyword. (Somewhat similar to the situation in Javascript.)
So you either have to remember to var every variable used in a function, including things like names that are using in a cfquery name, or you can just use this trick:
<cffunction name="MyFunction">
<cfset var Local = StructNew()>
<!--- Now anything Local. is automatically local --->
<cfset Local.x = 42>
<!--- Including cfquery name="" --->
<cfquery name="Local.Customers" datasource="some_datasource">
SELECT C.ID, C.Name
FROM Customers C
</cfquery>
</cffunction>
There's nothing magic about the name Local, it's just convention. Although Coldfusion 9 will add an explict local scope, so if you use Local it will probably ease upgrading to CF9 when the time comes.
Note that the situation is a tad different for CFCs: In CFCs, the variables scope (the "default" scope) isn't global like it is for normal functions but rather exists per instance of your CFC. So while forgetting to use var is not quite as dangerous in a CFC as it is in a top-level function, the best practice is still to use var all the time.

Failing to prevent users from seeing coldfusion errors.
Add a onError method to a top level Application.cfc to prevent users from seeing those all to detailed dump messages exposing your inner workings(and failings).
<cffunction name="onError" returntype="void" output="true">
<cfargument name="exception" type="any" required="true" />
<cfargument name="eventname" type="string" required="true" />
varscoper is also a great tool for automating the check for variable scoping omissions in components.
http://varscoper.riaforge.org/

Overuse of 'query of query'. That is, further filtering or sorting of query results using the cfquery tag.
This type of work is often done better by the database itself, particularly if the dataset is large.

One of the biggest mistakes would be not using cfqueryparam
Very bad:
SELECT UserName
FROM Customers
WHERE CustomerID = #URL.custid#
Very good:
SELECT UserName
FROM Customers
WHERE CustomerID = <cfqueryparam value="#URL.custid#" cfsqltype="cf_sql_integer">`
Making that mistake will cost you a website.

Putting variables in the wrong scope; even if you don't blow up the registry or crash the server, it's easy to slowly drain performance from your application by bumping variables up to the highest scope in which you think you might need them, or to lose information because you stored it in one scope and tried to access them in a different scope.
Using cfcatch without capturing and/or transmitting some information about the error so that it can be found and fixed. (It's difficult to find an error that doesn't tell you it occurred.)
Using listcontains() when you want listfind(). Especially if the list contains numbers. listfind() matches only an entire item in a list; listcontains() matches part of an item. (Yes, we made this mistake once.)
With administrator access:
Leaving the defaults for a data source set up on the server. "Least privileges" applies on the CF side as well; don't give it any more permissions than it specifically needs. (GRANT, ALTER, REVOKE, DROP ... you don't really want those checked.)
Not checking the boxes for retrieving all the contents from a CLOB/BLOB field when that's what you're expecting. (It was really interesting seeing that applied to a field in which we were storing PDFs.)

Related

ColdFusion doing OWASP esapi via Java

I am have some old ColdFusion code. It was originally written for CF9, but is now running on CF 2016.
application.cfc
local.esapi = createObject("java", "org.owasp.esapi.ESAPI");
application.esapiEncoder = local.esapi.encoder()
Much later
Regular page
form.Reason = application.esapiEncoder.encodeForHtml(form.Reason);
I am thinking of replacing this with
form.Reason = encodeForHTML(form.Reason);
Do these function the same?
Yes, the encodeForX() functions use OWASP's ESAPI behind the scenes. encodeForHTML() is CF10+ and has a canonicalize argument, which takes the input down to its lowest factor. CF2016 added an encodeFor argument to a cfoutput tag for outputting that does similar. There's also the canonicalize() function that will throw an error that you can catch. That's useful for seeing if someone is trying to throw nefarious inputs at your form or site. I can't think of a legit reason for double- or multi-encoding an input, and I would interpret such as an attack. The argument in the encodeForX() function will take it down to its base evaluation, but it doesn't throw an error and just returns the resulting output. Personally, I'm not sure that there's much of an accidental way to pass a value that would be picked up by canonicalization, and I'd simply rather catch that attempt and kick that user off of my site.
https://helpx.adobe.com/coldfusion/cfml-reference/coldfusion-functions/functions-e-g/encodeforhtml.html
https://helpx.adobe.com/coldfusion/cfml-reference/coldfusion-functions/functions-c-d/Canonicalize.html
https://www.owasp.org/index.php/Category:Encoding

Undocumented inconsistent behavior regarding ColdFusion's CGI scope/struct

I initially posted this as an answer to this question earlier regarding Empty CGI.REDIRECT_URL on ColdFusion 2016. After thinking about it, I thought better of it since technically didn't answer the OP's question. Instead I decided to make it into a separate question, even though it's more of a commentary than a question. While this technically might not meet the full requirements of a Minimal, Complete, and Verifiable example and people might ding me with downvotes, I decided it was worth it anyway in the hope that it will become easier to find for future CFers who might encounter this. Thus preventing them from banging their head against the wall regarding this peculiar behavior of the CGI struct/scope.
With that said, the CGI struct/scope has some undocumented inconsistent behavior from other structs/scopes. Note that I personally take no credit for this discovery since I happened across this some time ago upon reading Ben Nadel's blog post on this. So all the information I'm posting here is already detailed there, but I wanted to write a nice summary here on SO.
Undocumented behavior 1 - Unlike other structures, if a CGI struct key doesn't exist, then it won't throw an error when referencing it.
In the OP's original question, he was wondering why cgi.REDIRECT_URL existed but was empty. As he eventually found out, it never actually existed. As a separate example you can execute this line of code without throwing an error. Not what you'd expect, huh?
<cfoutout>#cgi.THIS_IS_A_FAKE_KEY#</cfoutout>
So what's a CFer to do? Test for the key existence.
<cfif structKeyExists( CGI, 'THIS_IS_A_FAKE_KEY' )>
THIS_IS_A_FAKE_KEY exists
<cfelse>
THIS_IS_A_FAKE_KEY doesn't exist
</cfif>
Undocumented behavior 2 - Unlike other structures, if you dump the CGI struct, it won't display all the key/value pairs, it will only display a defined set of keys.
In the OP's case, he had a custom Apache CGI variable cgi.REDIRECT_URL that was used in his code prior to upgrading to CF2016 and was able to refer to it directly. However, I'm presuming if he dumped out the cgi struct, it wouldn't appear in the dump. In Ben Nadel's case, he also had a custom cgi variable called cgi.document_root that was passed through from a load balancer and was able to refer to it directly, but he also wasn't able to see the key when dumping the cgi contents.
So what's a CFer to do? Understand this and store it in the back of your mind so you won't get bitten when you dump the cgi contents and the key/value pair isn't there. Other than that, not much else.
I went in to the cfusion.jar file of ColdFusion. What I found there was a bit confusing.
CGI scope is not of a structure form as one would hope for.
This is how a call for CGI variable is handled. eg <cfoutout>#cgi.THIS_IS_A_FAKE_KEY#</cfoutout>
The normal valid CGI scope variables are the ones in this list and these will be initialized to "" by default.
private static final String[] names ="AUTH_PASSWORD","AUTH_TYPE","AUTH_USER","CERT_COOKIE","CERT_FLAGS","CERT_ISSUER","CERT_KEYSIZE","CERT_SECRETKEYSIZE","CERT_SERIALNUMBER","CERT_SERVER_ISSUER","CERT_SERVER_SUBJECT","CERT_SUBJECT","CF_TEMPLATE_PATH","CONTENT_LENGTH","CONTENT_TYPE","CONTEXT_PATH","GATEWAY_INTERFACE","HTTP_ACCEPT","HTTP_ACCEPT_ENCODING","HTTP_ACCEPT_LANGUAGE","HTTP_CONNECTION","HTTP_COOKIE","HTTP_HOST","HTTP_USER_AGENT","HTTP_REFERER","HTTP_URL","HTTPS","HTTPS_KEYSIZE","HTTPS_SECRETKEYSIZE","HTTPS_SERVER_ISSUER","HTTPS_SERVER_SUBJECT","LOCAL_ADDR","PATH_INFO","PATH_TRANSLATED","QUERY_STRING","REMOTE_ADDR","REMOTE_HOST","REMOTE_USER","REQUEST_METHOD","SCRIPT_NAME","SERVER_NAME","SERVER_PORT","SERVER_PORT_SECURE","SERVER_PROTOCOL","SERVER_SOFTWARE","WEB_SERVER_API" };`
Also all of these values also come from various java libraries javax.servlet, HttpServletRequest etc.
If the requested variable is not any of these after a bit of checks, ColdFusion goes to the request headers. You can see these using getHttpRequestData().headers. Then looks for a key there with hyphens(-) instead of the underscores(_) in the cgi key request. (If the key starts with http_, then the key in the request headers will be with out it like this http_x_forward in request header will be x-forward)
value = request.getHeader(name.replace('_', '-'));
From what I understand as far as ColdFusion is concerned the keys, mentioned in the first point are the recognized as part of the CGI scope. But when there is additional information passed from a Apache load balancer server to ColdFusion, those end up in the request headers. Since the java getHeader just returns a blank string (or something with data type undefined) instead of a undefined error, ColdFusion does not identify any of the keys is defined or not.
So if the key THIS_IS_A_FAKE_KEY is sent to ColdFusion from an intermediary such as an Apache server. You will find that in the getHttpRequestData().headers['THIS-IS-A-FAKE-KEY'] but not on the CGI scope dump.
That being said my personal opinion is that it is better to check directly in the getHttpRequestData().headers for custom CGI variables other than in the scope itself.
EDIT Thanks to Ageax for pointing out one of my test cases was invalid on my earlier revision of this post.
Great bit of detective work RRK! So I decided to perform an experiment to verify your finding by creating two loops. The first loop will display the key/value pairs from getHttpRequestData().headers and the second loop does the same using the corresponding key/value pairs from the cgi scope by replacing the - with _. Voila! as reported by RRK, we can see how you can obtain the values by either method. I made an updated gist and posted here for anyone interested.
<cfset httpHeaders = getHttpRequestData().headers>
<h3>getHttpRequestData().headers</h3>
<cfloop collection="#httpHeaders#" item="key" >
<cfoutput><strong>#Key#</strong> : #httpHeaders[key]#<br></cfoutput>
</cfloop>
<h3>cgi keys dash to underscore</h3>
<cfloop collection="#httpHeaders#" item="key" >
<cfset keyUnderscore = replace(key, "-", "_", "all")>
<cfoutput><strong>#keyUnderscore#</strong> : #cgi[keyUnderscore]#<br></cfoutput>
</cfloop>

Fusebox invoking a fuse within the code

Does anyone know if its possible to invoke a fuseaction within a coldfusion template?
(You haven't specified which Fusebox version; this answer applies to Fusebox 5.x)
Your title and question is asking two different things - a fuse and a fuseaction are two distinct things. A fuse is simply a CFML template, whilst a fuseaction represents a bundle of logic that performs a particular action (similar to a function).
Fuses:
To invoke a fuse, simply include the file as you would normally - there's no special FB functionality required for this.
Fuseactions:
To invoke a fuseaction, use the do verb, like so:
<cfset myFusebox.do('circuit.fuseaction') />
To store the result, use the second argument for the content variable:
<cfset myFusebox.do('circuit.fuseaction',varname) />
This is the equivalent of this XML:
<do action="circuit.fuseaction" contentvariable="varname" />
There are other arguments available, see this Fusebox cheat sheet which contains plenty of other useful info too.
With MVC, you should be working through a single entry-point. So only a single fuseaction should be called during your request.
BUT that fuseaction can call some of the other model and view templates as needed. And I believe that Fusebox allows you to refactor that logic into something that can be used by multiple actions. (I'm a bit rusty on my Fusebox functionality though, but I bet some Googling will lead you the way.)
As a dire last resort, you could use <cfhttp> to call a URL within your app that invokes that fuseaction. But why not just run some of the code directly without needing to burden your server with another HTTP call?

Strategy/recommendations of using CFPARAM

Each time when using cfparam I have a kind of feeling that I put it in the wrong place or generally mis-using.
Consider this example, say we need to show the new entity form (with default input values):
<cfparam name="form.attachLink" default="" />
<input type="text" name="attachLink" value="#HTMLEditFormat(form.attachLink)#" />
Even this simple one makes me thinking about following questions on cfparam:
Should I use it before exact usage place? Wont it break the Model/View idea?
Or should I group them somewhere in model template, or maybe on top of the view?
When paramin' the form data, maybe it's better to use StructKeyExists(form, "attachLink")? Sure, I do this (plus validation) when processing the submitted form, but for the fresh form this can be useful too -- for safety-paranoid people like me.
Where else it makes sense to use this tag? I know one really useful place: custom tags, though they itself become more and more legacy too.
Thanks.
At the risk of sounding unhelpful: "It depends."
I think it's a bit complicated by the fact that CFPARAM can be used for setting default values as well as validating the type of a variable and throwing an error when it doesn't match. That said, I almost always use it just for the former. With the latter, a lot of that has been subsumed by arguments to components.
One of the more useful uses is on the action page for a form with checkboxes.
<cfparam name="form.myCheckbox" default="" />
Since it's valid for none of the checkboxes to be checked on the form, this prevents me from having to create special validation to see if the form variable exists before using it and, since I'm almost always treating it as a list, an empty-string is still valid for list functions.
As for where to place them, when I use them I almost always put them at the top of the cfm file, but that's probably just a style thing. If you sprinkle them in your code I think you're going to run into cases where the variable is going to be set and you don't know where it happened.
Of course, I'm using Model-Glue pretty much exclusively these days. Not much use for CFPARAM.
If you are going to reference the variable in a form value, you should use cfparam with a reasonable default (even blank) to ensure that the variable exists. However, if the variable is only being referenced upon form processing, I would skip cfparam (unnecessary as you're not setting a value in the form) and instead use structKeyExists(scope, "key") in the form processing step.
In an MVC framework, you may have an option to set your default values in another manner. In this case, you are most likely not referencing the form scope directly, but rather a framework managed scope. Fusebox uses the 'event' scope. On page load, the Fusebox framework merges both the URL and FORM scope together into the EVENT scope. If you are concerned about 'breaking MVC', I recommend setting up, and/or testing for the necessary variables in your framework's scope (in my case EVENT) in the MVC controller (using your framework's recommended method) before you display the form in the view. Most likely you won't need cfparam for this.
I think of cfparam the same way I think of variable declarations. I alway declare my variables as close as possible to where I'm going to use the variable. In coldfusion world, I use the cfparam tag right before the cfform tag.
It sounds like you're using an MVC framework. I don't see how this impacts the use of cfparam. The model is where you get the data. CFParam isn't specific to the model.

How to put records from a query into DAOs?

I have written a gateway to get a result set from my database. How do i store every row in a separate dao so that i can manipulate every record further? Or can i access the result set directly to get the records?
This is my Gateway (btw, should i write the conditional logic within the cfquery into a seperate cfc that extends this one?)
<cfcomponent name="MaterialDao" hint="data access object" output="false">
<cffunction name="init" hint="constructor" access="public" output="false" returntype="MaterialDao">
<cfargument name="dsn" type="String" required="true" hint="datasource" />
<cfset variables.instance.dsn = arguments.dsn />
<cfreturn this />
</cffunction>
<cffunction name="readMaterial" hint="read" access="public" output="false" returntype="Query">
<cfargument name="district" type="String" />
<cfset var qReadMaterial = "" />
<cfquery name="qReadMaterial" datasource="#variables.instance.dsn#">
<cfif StructKeyExists(arguments,"district")>
SELECT A.NR, A.BEZ, D.BES, D.STA
<cfelse>
SELECT A.NR, A.BEZ
</cfif>
FROM DEK AS D INNER JOIN ART AS A
ON D.NR = A.NR
WHERE 0=0
<cfif StructKeyExists(arguments,"district")>
AND D.BEZ = #arguments.district#
</cfif>
ORDER BY A.BEZ
</cfquery>
<cfreturn qReadMaterial />
</cffunction>
</cfcomponent>
I have already read a lot of articles and it seems that there are different opinions about this matter (DAO vs. Gateway, DAO & Gateway etc.). What is the best practice, what do the pros do?
The pros use just one pattern for the database access layer. The use of both a DAO and Gateway is a misnomer that I'm not really sure where it got started, but seems to only exist in the ColdFusion crowd. The DAO and Gateway patterns can pretty much serve the same function, but I think the DAO fits the bill more when talking about database interaction.
DAOs should include functionality for CRUD methods plus returning sets of records. Since CRUD and basic sets of records is highly repetitive, I use a code generator to create the code for this interaction and then customize what I need. This is a good place for conditional logic for selecting the records you need.
As Aaron mentioned, returning an array of objects for a set of records in your database is unfeasible in ColdFusion due the the performance overhead of creating objects. I typically just use the basic query returned from the DAO in my views. However, if the thing I'm modeling needs some behavior in a view, then I will put the query in an object using something similar to what Peter Bell does.
Peter Bell had a great presentation some months ago on his release of the Iterating Business Object CFC which allows you to take multiple records and iterate over one record at a time using this simple framework: http://ibo.riaforge.org/. Until CF is a little faster at generating objects, recycling a single instance of an object and repopulating the properties is likely your best best. Perhaps this can help you load one record at a time into your DAO.
Conditional logic can go in the Gateway or in a Manager CFC. Typically, I would include logic that is simple like the logic outlined in your post directly in the CFC.
A bit of advice, you may wish to make the arguments.distinct NOT required and do a simple check with if (structKeyExists(arguments, "distinct") ) { do something }.
Regards,
-Aaron Greenlee
At our company we thought long and hard about this stuff for a few months, trying the Adobe CF DAO creator via RDS and some other older ones (anyone remember CFPowerTools?).
We decided in the end to write our own DAO code generator and I thought I'd share our thoughts here. The reason we decided was because we needed to add locking hints to out SQL, we wanted to make it more efficient, more secure and cleaner.
The setup we decided on was to create a pre-defined base DAO object (called DAO.cfc) which all generated 'table' DAOs extended. All it had was a few utility methods, but the key thing thing is we can add any other functions in there that we need all our generated DAOs to have access to.
So we auto-generate code by selecting a table from the database (using the CF admin API) and create the [TableName].cfc DAO with the usual init, setters and getters, so the basic CRUD stuff.
In addition to this, we also generate a [TableName]GatewayBase.cfc and a [TableName]Gateway.cfc. [TableName]Gateway.cfc extends [TableName]GatewayBase.cfc.
So for a sample DAO run on a table called 'Customers', the files created are:
Customers.cfc /* extends DAO.cfc [not created, already exists] */
CustomersGateway.cfc
CustomersGatewayBase.cfc /* extends CustomersGateway */
So, the idea is that the gateway provides a way to deal with many 'Customer' records - the DAO is used when dealing with one and only one. All methods in the gateway will generally return a CF query object. CF is too inefficient to create massive arrays of DAO objects and in our mind the query object in CF is really flexible, so we're happy to use it.
When coding, the sub-class CustomerGateway.cfc is the only one instantiated and used. However, the base class it extends has some very useful generic functions that come for free, such as getFieldListByProperty() which based on passed parameters will return certain fields (i.e. table columns) by a certain property (i.e. a column value), so for example:
myGateway.getFieldListByProperty(property="status", value="1", fieldList="customerName,customerID", orderBy="createdOn") />
That call will return the 'customerName' and 'customerID' values for all customers with a status of 1, ordered by the date they were created on. The code is also hardened against SQL injection and validated so sensible exceptions are thrown.
This function will provide 99% (we hope!) of the multi-record querying you do on a table. If you need a more sophisiticated query then the CustomerGateway.cfc is there for you to add functions to.
Finally, we allow you add functions to CustomerGateway CFC only because if you change the customers table (say add a column), you will need to recreate the table, and tha will overwrite the Customers.cfc and the CustomersGatewayBase.cfc. However, your custom code (if any) is safe in the sub-class.
Anyway, this might be slightly off topic, but sure I thought someone might find our experience useful.