问题
My customer base is finally off Coldfusion 8 so now I can take advantage of Coldfusion 9's Application.cfc -> onCFCRequest
event. I have a test scenario setup and my result is not what I'm expecting. I have a method that I call which produces a valid XML
response like so...
Response Header: Content-Type:application/xml;charset=UTF-8
Response:
<?xml version="1.0" encoding="UTF-8"?>
<rows><row id="10000282742505"><cell/><cell> ...
Now after I introduce the onCFCRequest
event I get this back (which breaks my grids)...
Response Header: Content-Type:application/xml;charset=UTF-8
Response:
<wddxPacket version='1.0'><header/><data><string><rows><row id="10000282742505"><cell></cell><cell> ...
Here is the event...
<cffunction name="onCFCRequest" access="public" returntype="Any" output="true">
<cfargument type="string" name="cfc" required="true">
<cfargument type="string" name="method" required="true">
<cfargument type="struct" name="args" required="true">
<cfscript>
// OnCFCRequest security hole fix as detailed here: http://blog.adamcameron.me/2013/04/its-easy-to-create-security-hole-in.html
var o = createObject(ARGUMENTS.cfc);
var metadata = getMetadata(o[ARGUMENTS.method]);
if (structKeyExists(metadata, "access") && metadata.access == "remote"){
return invoke(o, ARGUMENTS.method, ARGUMENTS.args);
}else{
throw(type="InvalidMethodException", message="Invalid method called", detail="The method #method# does not exists or is inaccessible remotely");
}
</cfscript>
<cfreturn />
</cffunction>
How can I get onCFCRequest to pass through the response in the same format that the remote function returns?
I am aware of this article: http://www.bennadel.com/blog/1647-Learning-ColdFusion-9-Application-cfc-OnCFCRequest-Event-Handler-For-CFC-Requests.htm
I will probably end up trying that but first I'd like to figure out why I can't simply pass through the response in the same format.
回答1:
I've never used onCfcRequest
but you're right, it's kind of dumb.
Seems like onCfcRequest
will also "swallow" the returnFormat
, therefore you must implement your own returnFormat
detection and serialize to the correct format.
the ReturnType of the OnCFCRequest() method should be VOID just like its OnRequest() counter-part. Returning a value from this method does not seem to play any part in what actually gets returned in the page response. To return a value, you either have to output it within the method body, or stream it back via CFContent
Quoted from: http://www.bennadel.com/blog/1647-Learning-ColdFusion-9-Application-cfc-OnCFCRequest-Event-Handler-For-CFC-Requests.htm
E.g.
...
var result = invoke(o, ARGUMENTS.method, ARGUMENTS.args);
...
<!--- after your </cfscript> --->
<!--- TODO: do some checking to determine the preferred return type --->
<!--- if detected as xml, serve as xml (safer option) --->
<cfcontent type="application/xml"
variable="#toBinay(toBase64(local.result))#">
<!--- *OR* (cleaner version) watch out for white spaces --->
<cfcontent type="application/xml">
<cfoutput>#result#</cfoutput>
回答2:
It appears I can't simply pass through the result with the same format because of this... "By default, ColdFusion serializes all return types (including simple return types), except XML, into WDDX format, and returns XML data as XML text." (http://help.adobe.com/en_US/ColdFusion/9.0/CFMLRef/WSc3ff6d0ea77859461172e0811cbec22c24-7f5c.html). The remote function being called returns my xml
as a string to the onCFCRequest
, onCFCRequest
then converts that simple return type (a string at this point) into WDDX
because that's the default behavior.
So...
After a lot of testing I ended up going with the solution from Ben Nadel's article, but with a few tweaks I'd like to mention.
- The first addition was to add the code already shown in my question for the bug Adam Cameron found.
- The second addition was to do this right after the invoke:
<cfset local.result = ToString(local.result)>
. In Ben's comments it says "...all the values returned will be string..." but that doesn't seem to be the case. We have some remote functions that only return a number. Without theToString()
the code further down that converts the response to binary fails. - Down in the section where the mimeTypes are being set I altered the IF statement for
json
. In every remote function we've written we make a ColdFusion structure then return it like so:<cfreturn SerializeJSON(z.response) />
. This seems a lot easier than piecing together a json string manually, THEN serializing it in theonCFCRequest
. So in the onCFCRequest for the json mimeType I just treat it as a string because it's already serialized; so no need to serialize it second time. - Also in the mimeType section I added an IF statement for
xml
. We have many remote functions which spit outxml
for grids, notwddx
. And since there is not areturnFormat
ofxml
I added a check for thereturnType
ofxml
right above thewddx
check. - Changed the responseMimeType for
JSON
andXML
toapplication/json
andapplication/xml
per @Henry's comment. Thanks!
Thank you much Ben and Adam for laying the ground work!
Here is the final result...
<cffunction name="onCFCRequest" access="public" returntype="void" output="true" hint="I process the user's CFC request.">
<cfargument name="component" type="string" required="true" hint="I am the component requested by the user." />
<cfargument name="methodName" type="string" required="true" hint="I am the method requested by the user." />
<cfargument name="methodArguments" type="struct" required="true" hint="I am the argument collection sent by the user." />
<!---
Here we can setup any request level variables we want
and they will be accessible to all remote cfc calls.
--->
<cfset request.jspath = 'javascript'>
<cfset request.imgpath = 'images'>
<cfset request.csspath = 'css'>
<!---
Check to see if the target CFC exists in our cache.
If it doesn't then, create it and cached it.
--->
<cfif !structKeyExists( application.apiCache, arguments.component )>
<!---
Create the CFC and cache it via its path in the
application cache. This way, it will exist for
the life of the application.
--->
<cfset application.apiCache[ arguments.component ] = createObject( "component", arguments.component ) />
</cfif>
<!---
ASSERT: At this point, we know that the target
component has been created and cached in the
application.
--->
<!--- Get the target component out of the cache. --->
<cfset local.cfc = application.apiCache[ arguments.component ] />
<!--- Get the cfcs metaData --->
<cfset var metadata = getMetaData( local.cfc[ arguments.methodName ] )>
<!--- OnCFCRequest security hole fix as detailed here: http://cfmlblog.adamcameron.me/2013/04/its-easy-to-create-security-hole-in.html --->
<cfif structKeyExists(metadata, "access") and metadata.access eq "remote">
<!--- Good to go! --->
<cfelse>
<cfthrow type="InvalidMethodException" message="Invalid method called" detail="The method #arguments.methodName# does not exists or is inaccessible remotely">
</cfif>
<!---
Execute the remote method call and store the response
(note that if the response is void, it will destroy
the return variable).
--->
<cfinvoke returnvariable="local.result" component="#local.cfc#" method="#arguments.methodName#" argumentcollection="#arguments.methodArguments#" />
<!---
We have some functions that return only a number (ex: lpitems.cfc->get_lpno_onhandqty).
For those we must convert the number to a string, otherwise, when we try to
convert the response to binary down at the bottom of this function it will bomb.
--->
<cfset local.result = ToString(local.result)>
<!---
Create a default response data variable and mime-type.
While all the values returned will be string, the
string might represent different data structures.
--->
<cfset local.responseData = "" />
<cfset local.responseMimeType = "text/plain" />
<!---
Check to see if the method call above resulted in any
return value. If it didn't, then we can just use the
default response value and mime type.
--->
<cfif structKeyExists( local, "result" )>
<!---
Check to see what kind of return format we need to
use in our transformation. Keep in mind that the
URL-based return format takes precedence. As such,
we're actually going to PARAM the URL-based format
with the default in the function. This will make
our logic much easier to follow.
NOTE: This expects the returnFormat to be defined
on your CFC - a "best practice" with remote
method definitions.
--->
<cfparam name="url.returnFormat" type="string" default="#metadata.returnFormat#" />
<cfparam name="url.returnType" type="string" default="#metadata.returnType#" /> <!--- Added this line so we can check for returnType of xml --->
<!---
Now that we know the URL scope will have the
correct format, we can check that exclusively.
--->
<cfif (url.returnFormat eq "json")>
<!--- Convert the result to json. --->
<!---
We already serializeJSON in the function being called by the user, this would cause double encoding, so just treat as text
<cfset local.responseData = serializeJSON( local.result ) />
--->
<cfset local.responseData = local.result />
<!--- Set the appropriate mime type. --->
<cfset local.responseMimeType = "application/json" />
<!---
There is no returnFormat of xml so we will check returnType instead.
This leaves the door open for us to use wddx in future if we decide to.
--->
<cfelseif (url.returnType eq "xml")>
<!--- Convert the result to string. --->
<cfset local.responseData = local.result />
<!--- Set the appropriate mime type. --->
<cfset local.responseMimeType = "application/xml" />
<cfelseif (url.returnFormat eq "wddx")>
<!--- Convert the result to XML. --->
<cfwddx action="cfml2wddx" input="#local.result#" output="local.responseData" />
<!--- Set the appropriate mime type. --->
<cfset local.responseMimeType = "application/xml" />
<cfelse>
<!--- Convert the result to string. --->
<cfset local.responseData = local.result />
<!--- Set the appropriate mime type. --->
<cfset local.responseMimeType = "text/plain" />
</cfif>
</cfif>
<!---
Now that we have our response data and mime type
variables defined, we can stream the response back
to the client.
--->
<!--- Convert the response to binary. --->
<cfset local.binaryResponse = toBinary( toBase64( local.responseData ) ) />
<!---
Set the content length (to help the client know how
much data is coming back).
--->
<cfheader name="content-length" value="#arrayLen( local.binaryResponse )#" />
<!--- Stream the content. --->
<cfcontent type="#local.responseMimeType#" variable="#local.binaryResponse#" />
<!--- Return out. --->
<cfreturn />
</cffunction>
来源:https://stackoverflow.com/questions/22799587/coldfusion-oncfcrequest-changing-return-type-of-xml-into-wddx