Saturday, February 03, 2007

More URI-related UDFs

To follow up my parseUri() function, here are several more UDFs I've written recently to help with URI management:

  • getPageUri()
    Returns a struct containing the relative and absolute URIs of the current page. The difference between getPageUri().relative and CGI.SCRIPT_NAME is that the former will include the query string, if present.
  • matchUri(testUri, [masterUri])
    Returns a Boolean indicating whether or not two URIs are the same, disregarding the following differences:
    • Fragments (page anchors), e.g., "#top".
    • Inclusion of "index.cfm" in paths, e.g., "/dir/" vs. "/dir/index.cfm" (supports trailing query strings).
    If masterUri is not provided, the current page is used for comparison (supports both relative and absolute URIs).
  • replaceUriQueryKey(uri, key, substring)
    Replaces a URI query key and its value with a supplied key=value pair. Works with relative and absolute URIs, as well as standalone query strings (with or without a leading "?"). This is also used to support the following two UDFs:
  • addUriQueryKey(uri, key, value)
    Removes any existing instances of the supplied key, then appends it together with the provided value to the provided URI.
  • removeUriQueryKey(uri, key)
    Removes one or more query keys (comma delimited) and their values from the provided URI.

Now that I have these at my disposal, I frequently find myself using them in combination with each other, e.g.,
<a href="<cfoutput>#addUriQueryKey(
    getPageUri().relative,
    "key",
    "value"
)#</cfoutput>">Link</a>
.

Let me know if you find any of these useful…

<!--- Returns the relative and absolute URIs of the current page --->
<cffunction name="getPageUri" returntype="struct" output="FALSE">
    <cfset var pageProtocol = "http" />
    <cfset var pageQuery = "" />
    <cfset var uri = structNew() />
    
    <!--- Get the protocol of the current page --->
    <cfif CGI.HTTPS IS "ON">
        <cfset pageProtocol = "https" />
    </cfif>
    
    <!--- Get the query of the current page, including the leading question if the query is not empty --->
    <cfset pageQuery = reReplace("?" & CGI.QUERY_STRING, "\?$", "") />
    
    <!--- Construct the relative URI of the current page (excludes the protocol and domain) --->
    <cfset uri.relative = CGI.SCRIPT_NAME & pageQuery />
    <!--- Construct the absolute URI of the current page --->
    <cfset uri.absolute = pageProtocol & "://" & CGI.SERVER_NAME & uri.relative />
    
    <cfreturn uri />
</cffunction>

<!--- Returns a Boolean indicating whether or not two URIs are the same, disregarding the following differences:
• Fragments (page anchors), e.g., "#top".
• Inclusion of "index.cfm" in paths, e.g., "/dir/" vs. "/dir/index.cfm" (supports trailing query strings).
If masterUri is not provided, the current page is used for comparison (supports both relative and absolute URIs) --->
<cffunction name="matchUri" returntype="boolean" output="FALSE">
    <cfargument name="testUri" type="string" required="TRUE" />
    <cfargument name="masterUri" type="string" required="FALSE" default="" />
    
    <!--- If a masterUri was not provided --->
    <cfif len(masterUri) EQ 0>
        <!--- If testUri is an absolute URI --->
        <cfif reFindNoCase("^https?://", testUri) EQ 1>
            <cfset masterUri = getPageUri().absolute />
        <cfelse>
            <cfset masterUri = getPageUri().relative />
        </cfif>
    </cfif>
    
    <cfreturn reReplaceNoCase(reReplace(testUri, "##.*", ""), "/index\.cfm(?=\?|$)", "/", "ONE") IS reReplaceNoCase(reReplace(masterUri, "##.*", ""), "/index\.cfm(?=\?|$)", "/", "ONE") />
</cffunction>

<!--- Replace a URI query key and its value with a supplied key=value pair.
Works with relative and absolute URIs, as well as standalone query strings (with or without a leading "?") --->
<cffunction name="replaceUriQueryKey" returntype="string" output="FALSE">
    <cfargument name="uri" type="string" required="TRUE" />
    <cfargument name="key" type="string" required="TRUE" />
    <cfargument name="substring" type="string" required="TRUE" />
    <cfset var preQueryComponents = "" />
    <cfset var currentKey = "" />
    
    <!--- Remove any existing fragment (page anchor) from uri, since it will mess with our processing, and is unlikely to be relevant and/or correct in the new URI --->
    <cfset uri = reReplace(uri, "##.*", "", "ONE") />
    <!--- Store any pre-query URI components. For this to work, the string must start with "protocol:", "//authority", or "/" (path). Otherwise, we will assume the uri is comprised entirely of a query component --->
    <cfset preQueryComponents = reReplace(uri, "^((?:(?:[^:/?.]+:)?//[^/?]+)?(?:/[^?]*)?)?.*", "\1", "ONE") />
    <!--- Remove any pre-query components and the leading question mark from uri --->
    <cfset uri = reReplace(uri, "^(?:(?:[^:/?.]+:)?//[^/?]+)?(?:/[^?]*)?\??(.*)", "\1", "ONE") />
    <!--- Remove any superfluous ampersands in the query (this cleans up the query but is not required, and in any case this function doesn't generate superfluous ampersands) --->
    <cfset uri = reReplace(uri, "&(?=&)|&$", "", "ALL") />
    
    <!--- For each key specified, remove the corresponding key=value pair from uri. Note that key names which contain regex special characters (.,*,+,?,^,$,{,},(,),|,[,],\) which are not percent-encoded may behave unpredictably --->
    <cfloop index="currentKey" list="#key#" delimiters=",">
        <cfif len(currentKey) GT 0>
            <cfset uri = reReplaceNoCase(uri, ("(?:^|&)" & currentKey & "(?:=[^&]*)?"), "", "ALL") />
        </cfif>
    </cfloop>
    
    <!--- If we still have a value in uri after the above processing (beyond what we're about to add) --->
    <cfif len(uri) GT 0>
        <!--- Ensure the query is returned with only the necessary separator characters (? and &) --->
        <cfreturn (preQueryComponents & "?" & reReplace(uri, "^&", "") & reReplace("&" & substring, "&$", "")) />
    <cfelse>
        <!--- Append substring, including a leading question mark if substring is not empty --->
        <cfreturn (preQueryComponents & reReplace("?" & substring, "\?$", "")) />
    </cfif>
</cffunction>

<cffunction name="addUriQueryKey" returntype="string" output="FALSE">
    <cfargument name="uri" type="string" required="TRUE" />
    <cfargument name="key" type="string" required="TRUE" />
    <cfargument name="value" type="string" required="TRUE" />
    
    <!--- Until proper support is included for adding multiple keys with one call, use only the first key --->
    <cfset key = listFirst(key, ",") />
    
    <!--- Remove any existing instances of the key from uri, then add the new key=value pair.
    Do not include the trailing equals sign (=) if we're assigning an empty value to the added key --->
    <cfreturn replaceUriQueryKey(removeUriQueryKey(uri, key), "", (key & reReplace("=" & value, "=$", ""))) />
</cffunction>

<cffunction name="removeUriQueryKey" returntype="string" output="FALSE">
    <cfargument name="uri" type="string" required="TRUE" />
    <!--- Use a comma-delimited list to remove multiple keys with one call --->
    <cfargument name="key" type="string" required="TRUE" />
    
    <cfreturn replaceUriQueryKey(uri, key, "") />
</cffunction>

In other news, this cracked me up.

No comments: