A few months ago, I wrote about how to polyfill Lucee CFML’s form field grouping behavior using field names that end with []
. It’s such a great feature and I use it all the time for forms that have sets of related checkboxes and text fields. But, after chatting with Mary Jo Sminkey earlier this week, I wondered if the same technique could be applied to the URL search parameters.
The great news is, Lucee CFML supports this natively. Presumably, whatever they’re doing for form fields behind the scenes, they’re also doing for query string parameters.
And, the good news is, I was able to get this polyfilled in Adobe ColdFusion as well. It took some trial and error because the form
and url
parameters (from the underlying servlet) show up in different places depending on the HTTP method (GET
vs POST
). Not quite sure why that’s the case — I’m not a Java guy. I’m assuming that’s just how the servlet works?
Here’s the test ColdFusion page that I used to get this working. It has three forms on it:
- All parameters submitted via
GET
. - All parameters submitted via
POST
. - Parameters submitted via
GET
andPOST
.
In all cases, some subset of parameters are submitted using bracket notation for grouping. And I’m outputting the url
and form
scopes at the bottom of the processing:
GET Parameters Only
POST Parameters Only
Both GET And POST Parameters
Here’s a stitched-together screenshot of all three forms being submitted:

As you can see, the []
group notation worked perfectly in all three scenarios, including the last one in which both url
and form
scope parameters are being grouped in the same request.
While this works natively in Lucee CFML, I’m using the Application.cfc
ColdFusion component to polyfill this behavior in the onRequestStart()
life-cycle event:
component
output = false
hint = "I define the application settings and event handlers."
{
// Define the application settings.
this.name = "PolyfillInputGrouping";
this.applicationTimeout = createTimeSpan( 1, 0, 0, 0 );
this.sessionManagement = false;
this.setClientCookies = false;
this.passArrayByReference = true;
// ---
// LIFE-CYCLE METHODS.
// ---
/**
* I initialize each inbound HTTP request.
*/
public void function onRequestStart() {
// Note: in a production environment, this CFC can be cached.
new ParameterGroupingPolyfill().apply();
}
}
The Application.cfc
is just using the life-cycle events to make sure the polyfill gets applied. But, the logic for the polyfill is all contained within the ParameterGroupingPolyfill.cfc
component. Since this is just a demo, I’m instantiating the CFC on every request. But, in a production environment, I would cache it in a persisted scope.
Here’s the logic for the polyfill that works for both form
and url
grouping. It short-circuits the logic for Lucee CFML since it’s not needed; and, it throws an error in Boxlang since I couldn’t figure out how to access the underlying raw parameters:
component
hint = "I try to polyfill the parameter grouping functionality used by Lucee CFML"
{
/**
* I initialize the polyfill component.
*/
public void function init() {
variables.engines = {
ADOBE: "Adobe",
LUCEE: "Lucee",
BOXLANG: "Boxlang"
};
this.ENGINE = determineCfmlEngine();
this.IS_ADOBE = ( this.ENGINE == engines.ADOBE );
this.IS_LUCEE = ( this.ENGINE == engines.LUCEE );
this.IS_BOXLANG = ( this.ENGINE == engines.BOXLANG );
}
// ---
// PUBLIC METHODS.
// ---
/**
* I apply the parameter grouping polyfill to the CFML scopes.
*/
public void function apply() {
// Lucee already supports parameter grouping in both the form fields and the
// search parameters. As such, nothing needs to be done on a Lucee server.
if ( this.IS_LUCEE ) {
return;
}
// Boxlang doesn't appear to expose the underlying parameter map in the servlet
// wrapper. As such, we aren't able to polyfill the feature at this time (at least
// not until I talk to Brad Wood).
if ( this.IS_BOXLANG ) {
throw(
type = "Unsupported",
message = "Boxlang can't be polyfilled at this time.",
detail = "I haven't figured out how to access the raw request yet."
);
}
// Inspect the URL scope.
var keysToFix = findKeysToFix( url );
if ( keysToFix.len() ) {
fixScopeKeys( url, keysToFix, getRawUrlScope() );
}
// Inspect the FORM scope.
var keysToFix = findKeysToFix( form );
if ( keysToFix.len() ) {
fixScopeKeys( form, keysToFix, getRawFormScope() );
fixFieldNames();
}
}
// ---
// PRIVATE METHODS.
// ---
/**
* I determine which CFML engine is running.
*/
private string function determineCfmlEngine() {
if ( server.keyExists( "lucee" ) ) {
return engines.LUCEE;
}
if ( server.keyExists( "boxlang" ) ) {
return engines.BOXLANG;
}
return engines.ADOBE;
}
/**
* I identify which keys in the given scope need to be fixed. These are the keys that
* still have the "[]" suffix, indicating that the CFML engine didn't handle the
* grouping properly.
*/
private array function findKeysToFix( required struct cfmlScope ) {
return cfmlScope
.keyArray()
.filter( ( key ) => ( key.right( 2 ) == "[]" ) )
;
}
/**
* I remove any "[]" notation from the fieldnames property of the form.
*/
private void function fixFieldNames() {
form.fieldNames = form.fieldNames
.reReplace( "\[\](,|$)", "\1", "all" )
;
}
/**
* I replace the list-based value concatenation of the CFML scope with the array-based
* value aggregation in the given raw servlet parameters.
*/
private void function fixScopeKeys(
required struct cfmlScope,
required array keysToFix,
required struct rawParameters
) {
for ( var key in keysToFix ) {
// Remove the "[]" suffix from the key and create a new entry.
cfmlScope[ key.left( -2 ) ] =
// The underlying Java value is a native Java array. We need to convert
// that value to a native ColdFusion array (ArrayList) so that it will
// behave like any other array, complete with member methods.
arrayNew( 1 ).append( rawParameters[ key ], true )
;
// Swap the raw scope key-value pairs with the normalized versions.
cfmlScope.delete( key );
}
}
/**
* I get the underlying servlet form parameters.
*
* Caution: at this time, we assume this is the Adobe ColdFusion engine servlet
* implementation since it's the only one we can polyfill at this time.
*/
private struct function getRawFormScope() {
// Note: we're creating an intermediary struct in order to convert the raw servlet
// parameters into a CASE-INSENSITIVE collection. Without this, the key-casing in
// the corresponding CFML scope may not be accessible in the raw parameters.
var caseInsensitive = {};
if ( isGet() ) {
return caseInsensitive;
}
return caseInsensitive
.append( getServletRequest().getParameterMap() )
;
}
/**
* I get the underlying servlet search parameters.
*
* Caution: at this time, we assume this is the Adobe ColdFusion engine servlet
* implementation since it's the only one we can polyfill at this time.
*/
private struct function getRawUrlScope() {
// Note: we're creating an intermediary struct in order to convert the raw servlet
// parameters into a CASE-INSENSITIVE collection. Without this, the key-casing in
// the corresponding CFML scope may not be accessible in the raw parameters.
var caseInsensitive = {};
if ( isPost() ) {
return caseInsensitive
.append( getServletRequest().getRequest().getParameterMap() )
;
}
// Note: For a GET request, this will contain just the URL parameters. However,
// for PUT/PATCH/DELETE, this will be a combination of both URL and FORM values.
return caseInsensitive
.append( getServletRequest().getParameterMap() )
;
}
/**
* I get the underlying servlet request.
*/
private any function getServletRequest() {
return getPageContext().getRequest();
}
/**
* I determine if the current HTTP request is a get.
*/
private boolean function isGet() {
return ( cgi.request_method == "get" );
}
/**
* I determine if the current HTTP request is a post.
*/
private boolean function isPost() {
return ( cgi.request_method == "post" );
}
}
I’ll talk to the Ortus guys and see if I can get some insight in how to polyfill this for Boxlang.
Want to use code from this post?
Check out the license.
https://bennadel.com/4827
Source link