Coldfusion onCFCRequest Changing Return Type of XML Into WDDX

为君一笑 提交于 2019-12-05 20:57:44

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>

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.

  1. The first addition was to add the code already shown in my question for the bug Adam Cameron found.
  2. 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 the ToString() the code further down that converts the response to binary fails.
  3. 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 the onCFCRequest. 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.
  4. Also in the mimeType section I added an IF statement for xml. We have many remote functions which spit out xml for grids, not wddx. And since there is not a returnFormat of xml I added a check for the returnType of xml right above the wddx check.
  5. Changed the responseMimeType for JSON and XML to application/json and application/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>
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!