Skip to main content

Polyfill URL Search Parameter Grouping Using Bracket Notation In Adobe ColdFusion

By Web Design7 min read

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 and POST.

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:

Screenshot of browser after all three form submissions.

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