1551 lines
61 KiB
Plaintext
1551 lines
61 KiB
Plaintext
component hint="Your Application.cfc should extend this class" {
|
|
|
|
// this method is meant to be (optionally) overridden in your application.cfc
|
|
function getEnvironment() output="false" hint="override this function to define the current API environment" {
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* onTaffyRequest gives you the opportunity to inspect the request before it is sent to the service.
|
|
* If you override this function, you MUST either return TRUE or a representation object
|
|
* (eg either taffy.core.nativeJsonSerializer or your default representation class)
|
|
*/
|
|
function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers, methodMetadata, matchedURI) output="false" {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* onTaffyRequestEnd gives you the opportunity to access the request after it has been processed by the service.
|
|
* If you override this function, you MUST either return TRUE or a representation object
|
|
* (eg either taffy.core.nativeJsonSerializer or your default representation class)
|
|
*/
|
|
function onTaffyRequestEnd(verb, cfc, requestArguments, mimeExt, headers, methodMetadata, matchedURI, parsedResponse, originalResponse, statusCode) output="false" {
|
|
return true;
|
|
}
|
|
|
|
// override these functions to implement caching hooks
|
|
function validCacheExists(cacheKey) output="false" {
|
|
return false;
|
|
}
|
|
|
|
function setCachedResponse(cacheKey, data) output="false" {
|
|
}
|
|
|
|
function getCachedResponse(cacheKey) output="false" {
|
|
}
|
|
|
|
function getCacheKey(cfc, requestArguments, matchedURI) output="false" {
|
|
return arguments.matchedURI & "_" & arguments.requestArguments.hashCode();
|
|
}
|
|
|
|
// Your Application.cfc should override this method AND call super.onApplicationStart()
|
|
function onApplicationStart() {
|
|
var before = getTickCount();
|
|
var after = 0;
|
|
setupFramework();
|
|
checkEngineSupport();
|
|
after = getTickCount();
|
|
addTaffyHeader("X-TIME-TO-RELOAD", after - before);
|
|
return true;
|
|
}
|
|
|
|
// Your Application.cfc should override this method AND call super.onRequestStart(targetpath)
|
|
function onRequestStart(targetPath = "") {
|
|
var local = {};
|
|
request.unhandled = false;
|
|
local.reloadedInThisRequest = false;
|
|
request.taffyReloaded = false;
|
|
|
|
// this will probably happen if taffy is sharing an app name with an existing application so that you can use its application context
|
|
if (!structKeyExists(application, "_taffy")) {
|
|
onApplicationStart();
|
|
local.reloadedInThisRequest = true;
|
|
}
|
|
|
|
// allow reloading
|
|
if (
|
|
(
|
|
structKeyExists(url, application._taffy.settings.reloadKey)
|
|
&& url[application._taffy.settings.reloadKey] == application._taffy.settings.reloadPassword
|
|
)
|
|
|| application._taffy.settings.reloadOnEveryRequest == true
|
|
) {
|
|
if (!local.reloadedInThisRequest && !isUnhandledPathRequest(arguments.targetPath)) {
|
|
// prevent double reloads
|
|
onApplicationStart();
|
|
}
|
|
}
|
|
|
|
if (!isUnhandledPathRequest(arguments.targetPath)) {
|
|
// if browsing to root of api, show dashboard
|
|
local.path = replaceNoCase(cgi.path_info, cgi.script_name, "");
|
|
if (
|
|
!structKeyExists(url, application._taffy.settings.endpointURLParam)
|
|
&& !structKeyExists(form, application._taffy.settings.endpointURLParam)
|
|
&& len(local.path) <= 1
|
|
&& listFindNoCase(cgi.script_name, "index.cfm", "/") == listLen(cgi.script_name, "/")
|
|
) {
|
|
handleDashboardRequest();
|
|
}
|
|
} else {
|
|
// allow pass-thru for selected paths
|
|
structDelete(this, "onRequest");
|
|
structDelete(variables, "onRequest");
|
|
request.unhandled = true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// If you choose to override this function, consider calling super.onError(exception)
|
|
function onError(exception) {
|
|
var data = {};
|
|
var root = "";
|
|
var logger = "";
|
|
|
|
try {
|
|
if (structKeyExists(request, "unhandled") && request.unhandled == true) {
|
|
return super.onError(arguments.exception);
|
|
}
|
|
|
|
logger = createObject("component", application._taffy.settings.exceptionLogAdapter).init(
|
|
application._taffy.settings.exceptionLogAdapterConfig
|
|
);
|
|
logger.saveLog(exception);
|
|
|
|
// return 500 no matter what
|
|
cfheader(statuscode=500, statustext="Error");
|
|
cfcontent(reset=true);
|
|
|
|
if (structKeyExists(exception, "rootCause")) {
|
|
root = exception.rootCause;
|
|
} else {
|
|
root = exception;
|
|
}
|
|
|
|
if (structKeyExists(root, "TagContext")) {
|
|
data.stacktrace = root.tagContext;
|
|
}
|
|
|
|
if (application._taffy.settings.returnExceptionsAsJson == true) {
|
|
// try to find the relevant details
|
|
if (structKeyExists(root, "message")) {
|
|
data.error = root.message;
|
|
}
|
|
if (structKeyExists(root, "detail")) {
|
|
data.detail = root.detail;
|
|
}
|
|
if (structKeyExists(root, "tagContext")) {
|
|
data.tagContext = root.tagContext[1].template & " [Line #root.tagContext[1].line#]";
|
|
}
|
|
// MAKE IT LOOK GOOD!
|
|
setting showdebugoutput=false enablecfoutputonly=true;
|
|
cfcontent(type="application/json; charset=utf-8");
|
|
writeOutput(serializeJson(data));
|
|
}
|
|
} catch (any cfcatch) {
|
|
cfcontent(reset=true, type="text/plain; charset=utf-8");
|
|
cfheader(statuscode=500, statustext="Error");
|
|
var errorMsg = "An unhandled exception occurred: ";
|
|
if (isStruct(root) && structKeyExists(root, "message")) {
|
|
errorMsg &= root.message;
|
|
} else {
|
|
errorMsg &= root;
|
|
}
|
|
if (isStruct(root) && structKeyExists(root, "detail")) {
|
|
errorMsg &= " -- " & root.detail;
|
|
}
|
|
writeOutput(errorMsg);
|
|
writeDump(var=cfcatch, format="text", label="ERROR WHEN LOGGING EXCEPTION");
|
|
writeDump(var=exception, format="text", label="ORIGINAL EXCEPTION");
|
|
}
|
|
}
|
|
|
|
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
|
|
// short-circuit logic
|
|
function onRequest(required string targetPage) output="true" returntype="boolean" {
|
|
var _taffyRequest = {};
|
|
var local = {};
|
|
var m = "";
|
|
var sampler = "";
|
|
request._taffyRequest = _taffyRequest;
|
|
local.debug = false;
|
|
|
|
_taffyRequest.metrics = {};
|
|
m = _taffyRequest.metrics;
|
|
m.init = getTickCount();
|
|
|
|
// enable/disable debug output per settings
|
|
if (!structKeyExists(url, application._taffy.settings.debugKey)) {
|
|
setting showdebugoutput=false;
|
|
}
|
|
|
|
// display api dashboard if requested
|
|
if (
|
|
!structKeyExists(url, application._taffy.settings.endpointURLParam)
|
|
&& !structKeyExists(form, application._taffy.settings.endpointURLParam)
|
|
&& len(cgi.path_info) <= 1
|
|
&& listFindNoCase(cgi.script_name, "index.cfm", "/") == listLen(cgi.script_name, "/")
|
|
) {
|
|
handleDashboardRequest();
|
|
}
|
|
|
|
// get request details
|
|
m.beforeParse = getTickCount();
|
|
local.parsed = parseRequest();
|
|
m.afterParse = getTickCount();
|
|
structAppend(_taffyRequest, local.parsed);
|
|
m.parseTime = m.afterParse - m.beforeParse;
|
|
|
|
// CORS headers (so that CORS can pass even if the resource throws an exception)
|
|
local.allowVerbs = uCase(structKeyList(_taffyRequest.matchDetails.methods));
|
|
if (
|
|
(application._taffy.settings.allowCrossDomain == true || len(application._taffy.settings.allowCrossDomain) > 0)
|
|
&& listFindNoCase("PUT,PATCH,DELETE,OPTIONS", _taffyRequest.verb)
|
|
&& !listFind(local.allowVerbs, "OPTIONS")
|
|
) {
|
|
local.allowVerbs = listAppend(local.allowVerbs, "OPTIONS");
|
|
}
|
|
|
|
if (structKeyExists(_taffyRequest.headers, "origin") && (application._taffy.settings.allowCrossDomain == true || len(application._taffy.settings.allowCrossDomain) > 0)) {
|
|
if (application._taffy.settings.allowCrossDomain == true) {
|
|
cfheader(name="Access-Control-Allow-Origin", value="*");
|
|
} else {
|
|
// The Access-Control-Allow-Origin header can only have 1 value so we check to see if the Origin header is
|
|
// in the list of origins specified in the config setting and parrot back the Origin header if so.
|
|
// We also need to add the Access-Control-Allow-Credentials header and set it to true for those type requests
|
|
local.domains = listToArray(application._taffy.settings.allowCrossDomain, ", ;");
|
|
if (structKeyExists(_taffyRequest.headers, "origin")) {
|
|
for (local.i = 1; local.i <= arrayLen(local.domains); local.i++) {
|
|
if (lcase(rereplace(_taffyRequest.headers.origin, "(http|https):\/\/", "", "all")) == lcase(rereplace(local.domains[local.i], "(http|https):\/\/", "", "all"))) {
|
|
cfheader(name="Access-Control-Allow-Origin", value=_taffyRequest.headers.origin);
|
|
cfheader(name="Access-Control-Allow-Credentials", value="true");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
cfheader(name="Access-Control-Allow-Methods", value=local.allowVerbs);
|
|
// Why do we parrot back these headers? See: https://github.com/atuttle/Taffy/issues/144
|
|
if (!structKeyExists(_taffyRequest.headers, "Access-Control-Request-Headers")) {
|
|
cfheader(name="Access-Control-Allow-Headers", value="Origin, Authorization, X-CSRF-Token, X-Requested-With, Content-Type, X-HTTP-Method-Override, Accept, Referrer, User-Agent");
|
|
} else {
|
|
// parrot back all of the request headers to allow the request to continue (can we improve on this?)
|
|
local.allowedHeaders = {};
|
|
for (local.h in listToArray("Origin,Authorization,X-CSRF-Token,X-Requested-With,Content-Type,X-HTTP-Method-Override,Accept,Referrer,User-Agent")) {
|
|
local.allowedHeaders[local.h] = 1;
|
|
}
|
|
local.requestedHeaders = _taffyRequest.headers["Access-Control-Request-Headers"];
|
|
for (local.i in listToArray(local.requestedHeaders)) {
|
|
local.allowedHeaders[local.i] = 1;
|
|
}
|
|
cfheader(name="Access-Control-Allow-Headers", value=structKeyList(local.allowedHeaders));
|
|
}
|
|
}
|
|
|
|
// global headers
|
|
addHeaders(getGlobalHeaders());
|
|
|
|
// Now we know everything we need to know to service the request. let's service it!
|
|
|
|
// ...after we let the api developer know all of the request details first...
|
|
m.beforeOnTaffyRequest = getTickCount();
|
|
_taffyRequest.continue = onTaffyRequest(
|
|
_taffyRequest.verb,
|
|
_taffyRequest.matchDetails.beanName,
|
|
_taffyRequest.requestArguments,
|
|
_taffyRequest.returnMimeExt,
|
|
_taffyRequest.headers,
|
|
_taffyRequest.methodMetadata,
|
|
local.parsed.matchDetails.srcUri
|
|
);
|
|
m.afterOnTaffyRequest = getTickCount();
|
|
m.otrTime = m.afterOnTaffyRequest - m.beforeOnTaffyRequest;
|
|
|
|
if (!structKeyExists(_taffyRequest, "continue")) {
|
|
// developer forgot to return true
|
|
throw(
|
|
message="Error in your onTaffyRequest method",
|
|
detail="Your onTaffyRequest method returned no value. Expected: Return TRUE or call noData()/representationOf().",
|
|
errorcode="400"
|
|
);
|
|
}
|
|
|
|
if (isObject(_taffyRequest.continue)) {
|
|
// inspection complete but request has been aborted by developer; return custom response
|
|
_taffyRequest.result = duplicate(_taffyRequest.continue);
|
|
structDelete(_taffyRequest, "continue");
|
|
m.resourceTime = 0;
|
|
} else {
|
|
// inspection complete and request allowed by developer
|
|
|
|
// handle requests for simulated responses
|
|
if (structKeyExists(_taffyRequest.requestArguments, application._taffy.settings.simulateKey) && _taffyRequest.requestArguments[application._taffy.settings.simulateKey] == application._taffy.settings.simulatePassword) {
|
|
// is there a simulated response?
|
|
sampler = "sample#_taffyRequest.method#Response";
|
|
if (structKeyExists(_taffyRequest.matchDetails.metadata, sampler)) {
|
|
// get simulated response
|
|
_taffyRequest.result = invoke(
|
|
application._taffy.factory.getBean(_taffyRequest.matchDetails.beanName),
|
|
sampler
|
|
);
|
|
_taffyRequest.result = rep(_taffyRequest.result);
|
|
} else {
|
|
// no method for simulated response, so return 400
|
|
_taffyRequest.result = noData().withStatus(400, "No Sample Response Available");
|
|
}
|
|
} else {
|
|
// send request to service
|
|
if (structKeyExists(_taffyRequest.matchDetails.methods, _taffyRequest.verb)) {
|
|
// check the cache before we call the resource
|
|
m.cacheCheckTime = getTickCount();
|
|
local.cacheKey = getCacheKey(
|
|
_taffyRequest.matchDetails.beanName,
|
|
_taffyRequest.requestArguments,
|
|
local.parsed.matchDetails.srcUri
|
|
);
|
|
if (ucase(_taffyRequest.verb) == "GET" && validCacheExists(local.cacheKey)) {
|
|
m.cacheCheckTime = getTickCount() - m.cacheCheckTime;
|
|
m.cacheGetTime = getTickCount();
|
|
_taffyRequest.result = getCachedResponse(local.cacheKey);
|
|
m.cacheGetTime = getTickCount() - m.cacheGetTime;
|
|
} else {
|
|
if (ucase(_taffyRequest.verb) == "GET") {
|
|
m.cacheCheckTime = getTickCount() - m.cacheCheckTime;
|
|
} else {
|
|
structDelete(m, "cacheCheckTime");
|
|
}
|
|
// returns a representation-object
|
|
m.beforeResource = getTickCount();
|
|
_taffyRequest.result = invoke(
|
|
application._taffy.factory.getBean(_taffyRequest.matchDetails.beanName),
|
|
_taffyRequest.method,
|
|
_taffyRequest.requestArguments
|
|
);
|
|
m.afterResource = getTickCount();
|
|
m.resourceTime = m.afterResource - m.beforeResource;
|
|
if (!isDefined("_taffyRequest.result")) {
|
|
throw(
|
|
message="Resource did not return a value",
|
|
detail="The resource is expected to return a call to rep()/representationOf() or noData(). It appears there was no return at all.",
|
|
errorcode="taffy.resources.ResourceReturnsNothing"
|
|
);
|
|
}
|
|
// If the type returned is not an instance of baseSerializer, wrap it with a call to rep().
|
|
// This way we can directly return the object instead of a serializer from resource actions.
|
|
if (!isSerializerInstance(_taffyRequest.result)) {
|
|
_taffyRequest.result = rep(_taffyRequest.result);
|
|
}
|
|
if (ucase(_taffyRequest.verb) == "GET" && structKeyExists(local, "cacheKey")) {
|
|
m.cacheSaveStart = getTickCount();
|
|
setCachedResponse(local.cacheKey, _taffyRequest.result);
|
|
m.cacheSaveTime = getTickCount() - m.cacheSaveStart;
|
|
}
|
|
}
|
|
} else if (!listFind(local.allowVerbs, _taffyRequest.verb)) {
|
|
// if the verb is not implemented, refuse the request
|
|
cfheader(name="ALLOW", value=local.allowVerbs);
|
|
throwError(405, "Method Not Allowed");
|
|
} else {
|
|
// create dummy response for cross domain OPTIONS request
|
|
_taffyRequest.resultHeaders = {};
|
|
_taffyRequest.statusArgs = {};
|
|
_taffyRequest.statusArgs.statusCode = 200;
|
|
_taffyRequest.statusArgs.statusText = "OK";
|
|
}
|
|
}
|
|
}
|
|
|
|
// make sure the requested mime type is available
|
|
if (!mimeSupported(_taffyRequest.returnMimeExt)) {
|
|
throwError(400, "Requested format not available (#_taffyRequest.returnMimeExt#)");
|
|
}
|
|
|
|
if (structKeyExists(_taffyRequest, "result")) {
|
|
// get status code
|
|
_taffyRequest.statusArgs = {};
|
|
_taffyRequest.statusArgs.statusCode = _taffyRequest.result.getStatus();
|
|
_taffyRequest.statusArgs.statusText = _taffyRequest.result.getStatusText();
|
|
// get custom headers
|
|
_taffyRequest.resultHeaders = invoke(_taffyRequest.result, "getHeaders");
|
|
}
|
|
|
|
setting enablecfoutputonly=true;
|
|
cfcontent(reset=true, type="#getReturnMimeAsHeader(_taffyRequest.returnMimeExt)#; charset=utf-8");
|
|
cfheader(statuscode=_taffyRequest.statusArgs.statusCode, statustext=_taffyRequest.statusArgs.statusText);
|
|
|
|
// headers
|
|
addHeaders(_taffyRequest.resultHeaders);
|
|
|
|
// add ALLOW header for current resource, which describes available verbs
|
|
cfheader(name="ALLOW", value=local.allowVerbs);
|
|
|
|
// metrics headers that should always apply
|
|
addTaffyHeader("X-TIME-IN-PARSE", m.parseTime);
|
|
addTaffyHeader("X-TIME-IN-ONTAFFYREQUEST", m.otrTime);
|
|
if (structKeyExists(m, "resourceTime")) {
|
|
addTaffyHeader("X-TIME-IN-RESOURCE", m.resourceTime);
|
|
}
|
|
if (structKeyExists(m, "cacheCheckTime")) {
|
|
addTaffyHeader("X-TIME-IN-CACHE-CHECK", m.cacheCheckTime);
|
|
}
|
|
if (structKeyExists(m, "cacheGetTime")) {
|
|
addTaffyHeader("X-TIME-IN-CACHE-GET", m.cacheGetTime);
|
|
}
|
|
if (structKeyExists(m, "cacheSaveTime")) {
|
|
addTaffyHeader("X-TIME-IN-CACHE-SAVE", m.cacheSaveTime);
|
|
}
|
|
|
|
if (application._taffy.settings.exposeHeaders) {
|
|
local.exposeHeaderList = structKeyList(_taffyRequest.resultHeaders);
|
|
local.exposeHeaderValue = "";
|
|
if (application._taffy.settings.useEtags && _taffyRequest.verb == "GET" && _taffyRequest.result.getType() == "textual") {
|
|
local.exposeHeaderList = listAppend(local.exposeHeaderList, "Etag");
|
|
}
|
|
for (local.exposeHeader in listToArray(local.exposeHeaderList)) {
|
|
// filter out default simple response headers: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers
|
|
if (!listFindNoCase("Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma", local.exposeHeader)) {
|
|
local.exposeHeaderValue = listAppend(local.exposeHeaderValue, local.exposeHeader);
|
|
}
|
|
}
|
|
if (listLen(local.exposeHeaderValue) > 0) {
|
|
cfheader(name="Access-Control-Expose-Headers", value=local.exposeHeaderValue);
|
|
}
|
|
}
|
|
|
|
// result data
|
|
if (structKeyExists(_taffyRequest, "result")) {
|
|
_taffyRequest.resultType = _taffyRequest.result.getType();
|
|
local.resultSerialized = "";
|
|
|
|
if (_taffyRequest.resultType == "textual") {
|
|
// serialize the representation's data into the requested mime type
|
|
_taffyRequest.metrics.beforeSerialize = getTickCount();
|
|
_taffyRequest.resultSerialized = invoke(
|
|
_taffyRequest.result,
|
|
"getAs#_taffyRequest.returnMimeExt#"
|
|
);
|
|
_taffyRequest.metrics.afterSerialize = getTickCount();
|
|
m.serializeTime = m.afterSerialize - m.beforeSerialize;
|
|
addTaffyHeader("X-TIME-IN-SERIALIZE", m.serializeTime);
|
|
|
|
// apply jsonp wrapper if requested
|
|
if (structKeyExists(_taffyRequest, "jsonpCallback")) {
|
|
_taffyRequest.resultSerialized = _taffyRequest.jsonpCallback & "(" & _taffyRequest.resultSerialized & ");";
|
|
}
|
|
|
|
// don't return data if etags are enabled and the data hasn't changed
|
|
if (application._taffy.settings.useEtags && _taffyRequest.verb == "GET") {
|
|
// etag values are quoted per: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
|
|
if (structKeyExists(server, "lucee")) {
|
|
// hashCode() will not work for lucee, see issue #354
|
|
_taffyRequest.serverEtag = '"' & hash(_taffyRequest.resultSerialized) & '"';
|
|
} else {
|
|
_taffyRequest.serverEtag = '"' & _taffyRequest.result.getData().hashCode() & '"';
|
|
}
|
|
if (structKeyExists(_taffyRequest.headers, "If-None-Match")) {
|
|
_taffyRequest.clientEtag = _taffyRequest.headers["If-None-Match"];
|
|
|
|
if (len(_taffyRequest.clientEtag) > 0 && _taffyRequest.clientEtag == _taffyRequest.serverEtag) {
|
|
cfheader(statuscode=304, statustext="Not Modified");
|
|
cfcontent(reset=true, type="#application._taffy.settings.mimeExtensions[_taffyRequest.returnMimeExt]#; charset=utf-8");
|
|
return true;
|
|
} else {
|
|
cfheader(name="Etag", value=_taffyRequest.serverEtag);
|
|
}
|
|
} else {
|
|
cfheader(name="Etag", value=_taffyRequest.serverEtag);
|
|
}
|
|
}
|
|
|
|
m.done = getTickCount();
|
|
m.taffyTime = m.done - m.init - m.parseTime - m.otrTime - m.serializeTime;
|
|
if (structKeyExists(m, "resourceTime")) {
|
|
m.taffyTime -= m.resourceTime;
|
|
}
|
|
addTaffyHeader("X-TIME-IN-TAFFY", m.taffyTime);
|
|
|
|
cfcontent(reset=true, type="#application._taffy.settings.mimeExtensions[_taffyRequest.returnMimeExt]#; charset=utf-8");
|
|
if (_taffyRequest.resultSerialized != ('"' & '"')) {
|
|
local.resultSerialized = _taffyRequest.resultSerialized;
|
|
}
|
|
// debug output
|
|
if (structKeyExists(url, application._taffy.settings.debugKey)) {
|
|
local.debug = true;
|
|
}
|
|
|
|
} else if (_taffyRequest.resultType == "filename") {
|
|
m.done = getTickCount();
|
|
m.taffyTime = m.done - m.init - m.parseTime - m.otrTime - m.resourceTime;
|
|
addTaffyHeader("X-TIME-IN-TAFFY", m.taffyTime);
|
|
cfcontent(reset=true, file=_taffyRequest.result.getFileName(), type=_taffyRequest.result.getFileMime(), deletefile=_taffyRequest.result.getDeleteFile());
|
|
|
|
} else if (_taffyRequest.resultType == "filedata") {
|
|
m.done = getTickCount();
|
|
m.taffyTime = m.done - m.init - m.parseTime - m.otrTime - m.resourceTime;
|
|
addTaffyHeader("X-TIME-IN-TAFFY", m.taffyTime);
|
|
cfcontent(reset=true, variable=_taffyRequest.result.getFileData(), type=_taffyRequest.result.getFileMime());
|
|
|
|
} else if (_taffyRequest.resultType == "imagedata") {
|
|
m.done = getTickCount();
|
|
m.taffyTime = m.done - m.init - m.parseTime - m.otrTime - m.resourceTime;
|
|
addTaffyHeader("X-TIME-IN-TAFFY", m.taffyTime);
|
|
cfcontent(reset=true, variable=_taffyRequest.result.getImageData(), type=_taffyRequest.result.getFileMime());
|
|
}
|
|
}
|
|
|
|
local.resultSerialized = "";
|
|
if (structKeyExists(_taffyRequest, "resultSerialized")) {
|
|
local.resultSerialized = _taffyRequest.resultSerialized;
|
|
}
|
|
|
|
local.result = {};
|
|
if (structKeyExists(_taffyRequest, "result")) {
|
|
local.result = _taffyRequest.result.getData();
|
|
}
|
|
|
|
// ...after the service has finished...
|
|
m.beforeOnTaffyRequestEnd = getTickCount();
|
|
onTaffyRequestEnd(
|
|
_taffyRequest.verb,
|
|
_taffyRequest.matchDetails.beanName,
|
|
_taffyRequest.requestArguments,
|
|
_taffyRequest.returnMimeExt,
|
|
_taffyRequest.headers,
|
|
_taffyRequest.methodMetadata,
|
|
local.parsed.matchDetails.srcUri,
|
|
local.resultSerialized,
|
|
local.result,
|
|
_taffyRequest.statusArgs.statusCode
|
|
);
|
|
m.otreTime = getTickCount() - m.beforeOnTaffyRequestEnd;
|
|
addTaffyHeader("X-TIME-IN-ONTAFFYREQUESTEND", m.otreTime);
|
|
|
|
if (len(trim(local.resultSerialized))) {
|
|
writeOutput(local.resultSerialized);
|
|
}
|
|
// debug output
|
|
if (local.debug) {
|
|
writeOutput("<h3>Request Details:</h3>");
|
|
writeDump(var=_taffyRequest);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
// ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
|
|
// internal methods
|
|
private void function checkEngineSupport() output="false" {
|
|
application._taffy.compat = {
|
|
queryToStruct: "missing",
|
|
queryToArray: "missing"
|
|
};
|
|
var funcs = getFunctionList();
|
|
if (structKeyExists(funcs, "queryToStruct")) {
|
|
application._taffy.compat.queryToStruct = "exists";
|
|
}
|
|
if (structKeyExists(funcs, "queryToArray")) {
|
|
application._taffy.compat.queryToArray = "exists";
|
|
}
|
|
}
|
|
|
|
private void function setupFramework() output="false" {
|
|
var local = {};
|
|
param name="variables.framework" default={};
|
|
request.taffyReloaded = true;
|
|
local._taffy = {};
|
|
local._taffy.version = "4.0.0";
|
|
local._taffy.endpoints = {};
|
|
|
|
// default settings
|
|
local.defaultConfig = {};
|
|
local.defaultConfig.resourcesCFCPath = "";
|
|
local.defaultConfig.docs = {};
|
|
local.defaultConfig.docs.APIName = "Your API Name (variables.framework.docs.APIName)";
|
|
local.defaultConfig.docs.APIVersion = "0.0.0 (variables.framework.docs.APIVersion)";
|
|
local.defaultConfig.docsPath = "../dashboard/docs.cfm";
|
|
local.defaultConfig.defaultMime = "";
|
|
local.defaultConfig.debugKey = "debug";
|
|
local.defaultConfig.reloadKey = "reload";
|
|
local.defaultConfig.reloadPassword = "true";
|
|
local.defaultConfig.reloadOnEveryRequest = false;
|
|
local.defaultConfig.simulateKey = "sampleResponse";
|
|
local.defaultConfig.simulatePassword = "true";
|
|
local.defaultConfig.endpointURLParam = "endpoint";
|
|
local.defaultConfig.serializer = "taffy.core.nativeJsonSerializer";
|
|
local.defaultConfig.deserializer = "taffy.core.nativeJsonDeserializer";
|
|
local.defaultConfig.disableDashboard = false;
|
|
local.defaultConfig.disabledDashboardRedirect = "";
|
|
local.defaultConfig.dashboardHeaders = {};
|
|
local.defaultConfig.showDocsWhenDashboardDisabled = false;
|
|
local.defaultConfig.unhandledPaths = "/flex2gateway";
|
|
local.defaultConfig.allowCrossDomain = false;
|
|
local.defaultConfig.useEtags = false;
|
|
local.defaultConfig.exposeHeaders = false;
|
|
local.defaultConfig.jsonp = false;
|
|
local.defaultConfig.noDataSends204NoContent = false;
|
|
local.defaultConfig.exposeTaffyHeaders = true;
|
|
local.defaultConfig.globalHeaders = {};
|
|
local.defaultConfig.mimeTypes = {};
|
|
local.defaultConfig.returnExceptionsAsJson = false;
|
|
local.defaultConfig.exceptionLogAdapter = "taffy.bonus.LogToDevNull";
|
|
local.defaultConfig.exceptionLogAdapterConfig = {};
|
|
local.defaultConfig.csrfToken = {};
|
|
local.defaultConfig.csrfToken.cookieName = "";
|
|
local.defaultConfig.csrfToken.headerName = "";
|
|
local.defaultConfig.allowGoogleFonts = true;
|
|
// optional keys: description, contact, license, servers
|
|
local.defaultConfig.openapi = { "enabled": true };
|
|
|
|
// status
|
|
local._taffy.status = {};
|
|
local._taffy.status.internalBeanFactoryUsed = false;
|
|
local._taffy.status.externalBeanFactoryUsed = false;
|
|
local._taffy.uriMatchOrder = [];
|
|
|
|
// allow setting overrides
|
|
local._taffy.settings = {};
|
|
structAppend(local._taffy.settings, local.defaultConfig, true); // initialize to default values
|
|
structAppend(local._taffy.settings, variables.framework, true); // update with user values
|
|
|
|
// allow environment-specific config
|
|
local.env = getEnvironment();
|
|
if (len(local.env) > 0) {
|
|
param name="variables.framework" default={};
|
|
param name="variables.framework.environments" default={};
|
|
if (structKeyExists(variables.framework.environments, local.env) && isStruct(variables.framework.environments[local.env])) {
|
|
structAppend(local._taffy.settings, variables.framework.environments[local.env]);
|
|
}
|
|
}
|
|
|
|
// bean factory definition
|
|
if (structKeyExists(variables.framework, "beanFactory")) {
|
|
if (isSimpleValue(variables.framework.beanFactory) && len(variables.framework.beanFactory) == 0) {
|
|
// if the BF value is "" doing nothing with it
|
|
} else {
|
|
local._taffy.externalBeanFactory = variables.framework.beanFactory;
|
|
local._taffy.status.externalBeanFactoryUsed = true;
|
|
}
|
|
}
|
|
|
|
// translate unhandledPaths config to regex for easier matching (This is ripped off from FW/1. Thanks, Sean!)
|
|
local._taffy.settings.unhandledPathsRegex = replaceNoCase(
|
|
REReplace(local._taffy.settings.unhandledPaths, "(\+|\*|\?|\.|\[|\^|\$|\(|\)|\{|\||\\)", "\\\1", "all"),
|
|
",", "|", "all"
|
|
);
|
|
|
|
// if resources folder exists, use internal bean factory
|
|
local.resourcePath = guessResourcesFullPath(local._taffy.settings.resourcesCFCPath);
|
|
local.noResources = false;
|
|
if (directoryExists(local.resourcePath)) {
|
|
// setup internal bean factory
|
|
local._taffy.factory = createObject("component", "taffy.core.factory");
|
|
if (structKeyExists(local._taffy, "externalBeanFactory")) {
|
|
local._taffy.factory.init(local._taffy.externalBeanFactory);
|
|
} else {
|
|
local._taffy.factory.init();
|
|
}
|
|
local._taffy.factory.loadBeansFromPath(local.resourcePath, guessResourcesCFCPath(local._taffy.settings.resourcesCFCPath), local.resourcePath, true, local._taffy);
|
|
local._taffy.beanList = local._taffy.factory.getBeanList();
|
|
local._taffy.endpoints = cacheBeanMetaData(local._taffy.factory, local._taffy.beanList, local._taffy);
|
|
local._taffy.status.internalBeanFactoryUsed = true;
|
|
} else if (local._taffy.status.externalBeanFactoryUsed) {
|
|
// only using external factory, so create a pointer to it
|
|
local._taffy.factory = local._taffy.externalBeanFactory;
|
|
// since external factory is only factory, check it for taffy resources
|
|
local.beanList = getBeanListFromExternalFactory(local._taffy.externalBeanFactory);
|
|
local._taffy.endpoints = cacheBeanMetaData(local._taffy.externalBeanFactory, local.beanList, local._taffy);
|
|
} else {
|
|
local.noResources = true;
|
|
}
|
|
|
|
if (!local.noResources) {
|
|
// sort URIs
|
|
local._taffy.URIMatchOrder = sortURIMatchOrder(local._taffy.endpoints);
|
|
// automatically introspect mime types from cfc metadata of default representation class
|
|
local.mimetypes = inspectMimeTypes(local._taffy.settings.serializer, local._taffy.factory);
|
|
structAppend(local._taffy.settings, local.mimeTypes);
|
|
// check to make sure a default mime type is set
|
|
if (local._taffy.settings.defaultMime == "") {
|
|
throwError(400, "You have not specified a default mime type!");
|
|
}
|
|
}
|
|
|
|
// inspect the deserializer to find out what contentTypes are supported
|
|
local._taffy.contentTypes = getSupportedContentTypes(
|
|
local._taffy.settings.deserializer,
|
|
structKeyExists(local._taffy, "factory") ? local._taffy.factory : ""
|
|
);
|
|
// hot-swap!
|
|
application._taffy = local._taffy;
|
|
|
|
// we must write the header *after* we have initialized our settings
|
|
addTaffyHeader("X-TAFFY-RELOADED", true);
|
|
}
|
|
|
|
private struct function parseRequest() output="false" {
|
|
var requestObj = {};
|
|
var tmp = 0;
|
|
var local = {};
|
|
|
|
// Check for method tunnelling by clients unable to send PUT/DELETE requests (e.g. Flash Player);
|
|
// Actual desired method will be contained in a special header
|
|
var httpMethodOverride = "null";
|
|
try {
|
|
httpMethodOverride = GetPageContext().getRequest().getHeader("X-HTTP-Method-Override");
|
|
} catch (any e) {
|
|
}
|
|
|
|
requestObj.uri = getPath();
|
|
if (!len(requestObj.uri)) {
|
|
if (structKeyExists(url, application._taffy.settings.endpointURLParam)) {
|
|
requestObj.uri = urlDecode(url[application._taffy.settings.endpointURLParam]);
|
|
} else if (structKeyExists(form, application._taffy.settings.endpointURLParam)) {
|
|
requestObj.uri = urlDecode(form[application._taffy.settings.endpointURLParam]);
|
|
}
|
|
}
|
|
|
|
// check for format in the URI
|
|
requestObj.uriFormat = formatFromURI(requestObj.uri);
|
|
|
|
// attempt to find the cfc for the requested uri
|
|
requestObj.matchingRegex = matchURI(requestObj.uri);
|
|
|
|
// uri doesn't map to any known resources
|
|
if (!len(requestObj.matchingRegex)) {
|
|
throwError(404, "Not Found");
|
|
}
|
|
|
|
// get the cfc name and token array for the matching regex
|
|
requestObj.matchDetails = application._taffy.endpoints[requestObj.matchingRegex];
|
|
|
|
// which verb is requested?
|
|
requestObj.verb = cgi.request_method;
|
|
|
|
// Should we override the actual method based on method tunnelling?
|
|
if (isDefined("httpMethodOverride") && !isNull(httpMethodOverride) && httpMethodOverride != "null") {
|
|
requestObj.verb = httpMethodOverride;
|
|
}
|
|
|
|
if (structKeyExists(application._taffy.endpoints[requestObj.matchingRegex].methods, requestObj.verb)) {
|
|
requestObj.method = application._taffy.endpoints[requestObj.matchingRegex].methods[requestObj.verb];
|
|
} else {
|
|
requestObj.method = "";
|
|
}
|
|
|
|
requestObj.body = getRequestBody();
|
|
requestObj.contentType = cgi.content_type;
|
|
if (len(requestObj.body) && requestObj.body != "null") {
|
|
if (findNoCase("multipart/form-data", requestObj.contentType)) {
|
|
// do nothing, to support the way lucee handles multipart requests (just avoids the error condition below)
|
|
requestObj.queryString = cgi.query_string;
|
|
} else {
|
|
if (contentTypeIsSupported(requestObj.contentType)) {
|
|
requestObj.bodyArgs = getDeserialized(requestObj.body, requestObj.contentType);
|
|
requestObj.queryString = cgi.query_string;
|
|
} else {
|
|
if (isJson(requestObj.body)) {
|
|
throwError(400, "Looks like you're sending JSON data, but you haven't specified a content type. Aborting request.");
|
|
} else {
|
|
throwError(400, "You must specify a content-type. Aborting request.");
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// actual query parameters
|
|
requestObj.queryString = cgi.query_string;
|
|
}
|
|
|
|
// grab request headers
|
|
requestObj.headers = getHTTPRequestData().headers;
|
|
|
|
// build the argumentCollection to pass to the cfc
|
|
requestObj.requestArguments = buildRequestArguments(
|
|
requestObj.matchingRegex,
|
|
requestObj.matchDetails.tokens,
|
|
requestObj.uri,
|
|
requestObj.queryString,
|
|
requestObj.headers
|
|
);
|
|
// include any deserialized body params
|
|
if (structKeyExists(requestObj, "bodyArgs")) {
|
|
structAppend(requestObj.requestArguments, requestObj.bodyArgs);
|
|
}
|
|
// also capture form POST data (higher priority that url variables of same name)
|
|
structAppend(requestObj.requestArguments, form);
|
|
|
|
// if JSONP is enabled, capture the requested callback name
|
|
if (application._taffy.settings.jsonp != false) {
|
|
if (structKeyExists(requestObj.requestArguments, application._taffy.settings.jsonp)) {
|
|
local.cbName = requestObj.requestArguments[application._taffy.settings.jsonp];
|
|
// validate callback name to prevent XSS via JSONP injection
|
|
if (reFindNoCase("^[a-zA-Z_$][a-zA-Z0-9_$.]*$", local.cbName)) {
|
|
requestObj.jsonpCallback = local.cbName;
|
|
} else {
|
|
throwError(400, "Invalid JSONP callback name");
|
|
}
|
|
}
|
|
}
|
|
|
|
// use requested mime type or the default
|
|
requestObj.returnMimeExt = "";
|
|
if (structKeyExists(requestObj.requestArguments, "_taffy_mime")) {
|
|
requestObj.returnMimeExt = requestObj.requestArguments._taffy_mime;
|
|
if (left(requestObj.returnMimeExt, 1) == ".") {
|
|
requestObj.returnMimeExt = right(requestObj.returnMimeExt, len(requestObj.returnMimeExt) - 1);
|
|
}
|
|
if (requestObj.returnMimeExt == "*/*") {
|
|
requestObj.returnMimeExt = application._taffy.settings.defaultMime;
|
|
}
|
|
if (!structKeyExists(application._taffy.settings.mimeExtensions, requestObj.returnMimeExt)) {
|
|
throwError(400, "Requested mime type is not supported (#requestObj.returnMimeExt#)");
|
|
}
|
|
} else if (requestObj.uriFormat != "") {
|
|
requestObj.returnMimeExt = requestObj.uriFormat;
|
|
} else {
|
|
// run some checks on the default
|
|
if (application._taffy.settings.defaultMime == "") {
|
|
throwError(400, "You have not specified a default mime type");
|
|
} else if (!structKeyExists(application._taffy.settings.mimeExtensions, application._taffy.settings.defaultMime)) {
|
|
throwError(400, "Your default mime type (#application._taffy.settings.defaultMime#) is not implemented");
|
|
}
|
|
requestObj.returnMimeExt = application._taffy.settings.defaultMime;
|
|
}
|
|
structDelete(requestObj.requestArguments, "_taffy_mime");
|
|
|
|
// get the method metadata (if any) for onTaffyRequest
|
|
if (requestObj.method != "") {
|
|
requestObj.methodMetadata = application._taffy.endpoints[requestObj.matchingRegex].metadata[requestObj.method];
|
|
} else {
|
|
// method will be "" when un-implemented method is requested (e.g. OPTIONS)
|
|
requestObj.methodMetadata = {};
|
|
}
|
|
|
|
return requestObj;
|
|
}
|
|
|
|
private function formatFromURI(uri) output="false" {
|
|
var local = {};
|
|
for (local.mime in application._taffy.settings.mimeExtensions) {
|
|
if (right(arguments.uri, len(local.mime) + 1) == "." & local.mime) {
|
|
return local.mime;
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
private function convertURItoRegex(required string uri) output="false" {
|
|
var local = {};
|
|
|
|
local.uriChunks = listToArray(arguments.uri, "/");
|
|
local.returnData = {};
|
|
local.returnData.tokens = [];
|
|
local.uriMatcher = "";
|
|
|
|
local.regexes = {};
|
|
local.regexes.segment = "([^\/]+)"; // anything but a slash
|
|
local.regexes.segmentWithOptFormat = "(?:(?:([^\/\.]+)(?:\.)([a-zA-Z0-9]+))\/?|([^\/\.]+))";
|
|
local.regexes.optFormatWithOptSlash = "((?:\.)[^\.\?\/]+)?\/?" // for ".json[/]" support
|
|
|
|
/*
|
|
above regex explained:
|
|
(?:
|
|
(?:
|
|
([^\/]+)(?:\.)([a-zA-Z0-9]+) --foo.json
|
|
)
|
|
\/? --optional trailing slash
|
|
|( --or
|
|
([^\/]+) --foo
|
|
)
|
|
)
|
|
|
|
we make it this complicated so that we can capture the ".json" separately from the "foo"
|
|
... fucking regex, man!
|
|
*/
|
|
|
|
for (local.chunk in local.uriChunks) {
|
|
if (left(local.chunk, 1) != "{" || right(local.chunk, 1) != "}") {
|
|
// not a token
|
|
local.uriMatcher = local.uriMatcher & "/" & local.chunk;
|
|
} else {
|
|
// strip {curly braces}
|
|
local.chunk = left(right(local.chunk, len(local.chunk) - 1), len(local.chunk) - 2);
|
|
// it's a token... but which kind?
|
|
if (find(":", local.chunk) != 0) {
|
|
local.pattern = "(" & listRest(local.chunk, ":") & ")"; // make sure we capture the value
|
|
local.tokenName = listFirst(local.chunk, ":");
|
|
} else {
|
|
local.pattern = local.regexes.segment;
|
|
local.tokenName = local.chunk;
|
|
}
|
|
local.uriMatcher = local.uriMatcher & "/" & local.pattern;
|
|
arrayAppend(local.returnData.tokens, local.tokenName);
|
|
}
|
|
}
|
|
|
|
// if uriRegex ends with a token, slip the format piece in there too...
|
|
local.uriRegex = "^" & local.uriMatcher;
|
|
if (right(local.uriRegex, 8) == local.regexes.segment) {
|
|
local.uriRegex = left(local.uriRegex, len(local.uriRegex) - 8) & local.regexes.segmentWithOptFormat;
|
|
}
|
|
|
|
// require the uri to terminate after specified content
|
|
local.uriRegex = local.uriRegex & local.regexes.optFormatWithOptSlash & "$";
|
|
|
|
local.returnData.uriRegex = local.uriRegex;
|
|
return local.returnData;
|
|
}
|
|
|
|
private function sortURIMatchOrder(endpoints) output="false" {
|
|
var URIMatchOrder = listToArray(structKeyList(arguments.endpoints, chr(10)), chr(10));
|
|
arraySort(URIMatchOrder, "textnocase", "desc");
|
|
return URIMatchOrder;
|
|
}
|
|
|
|
private string function matchURI(required string requestedURI) output="false" {
|
|
var local = {};
|
|
local.uriCount = arrayLen(application._taffy.URIMatchOrder);
|
|
for (local.i = 1; local.i <= local.uriCount; local.i++) {
|
|
local.attempt = reMatchNoCase(application._taffy.URIMatchOrder[local.i], arguments.requestedURI);
|
|
if (arrayLen(local.attempt) > 0) {
|
|
// found our mapping
|
|
return application._taffy.URIMatchOrder[local.i];
|
|
}
|
|
}
|
|
// nothing found
|
|
return "";
|
|
}
|
|
|
|
private function getRequestBody() output="false" hint="Gets request body data, which CF doesn't do automatically for some verbs" {
|
|
// Special thanks to Jason Dean (@JasonPDean) and Ray Camden (@ColdFusionJedi) who helped me figure out how to do this
|
|
var body = getHTTPRequestData().content;
|
|
// on input with content-type "application/json" CF seems to expose it as binary data. Here we convert it back to plain text
|
|
if (isBinary(body)) {
|
|
body = charsetEncode(body, "UTF-8");
|
|
}
|
|
return body;
|
|
}
|
|
|
|
private function contentTypeIsSupported(string contentType) output="false" {
|
|
var ct = listFirst(arguments.contentType, ";");
|
|
return structKeyExists(application._taffy.contentTypes, ct);
|
|
}
|
|
|
|
private function getDeserialized(required body, required contentType) output="false" {
|
|
var ct = listFirst(arguments.contentType, ";");
|
|
var fn = application._taffy.contentTypes[ct];
|
|
var args = {};
|
|
var result = {};
|
|
var deserializer = "";
|
|
args.body = arguments.body;
|
|
|
|
if (application._taffy.factory.containsBean(application._taffy.settings.deserializer)) {
|
|
deserializer = application._taffy.factory.getBean(application._taffy.settings.deserializer);
|
|
} else {
|
|
deserializer = createObject("component", application._taffy.settings.deserializer);
|
|
}
|
|
result = invoke(deserializer, fn, args);
|
|
return result;
|
|
}
|
|
|
|
private function getSupportedContentTypes(deserializer, factory = "") output="false" {
|
|
if (isObject(arguments.factory) && arguments.factory.containsBean(arguments.deserializer)) {
|
|
return _recurse_getSupportedContentTypes(getMetadata(arguments.factory.getBean(arguments.deserializer)));
|
|
} else {
|
|
return _recurse_getSupportedContentTypes(getComponentMetadata(arguments.deserializer));
|
|
}
|
|
}
|
|
|
|
private function _recurse_getSupportedContentTypes(objMetaData) output="false" {
|
|
var local = {};
|
|
local.response = {};
|
|
// recurse into parents first so that children override parents
|
|
if (structKeyExists(arguments.objMetaData, "extends")) {
|
|
local.response = _recurse_getSupportedContentTypes(arguments.objMetaData.extends);
|
|
}
|
|
// then handle child settings
|
|
if (structKeyExists(arguments.objMetaData, "functions") && isArray(arguments.objMetaData.functions)) {
|
|
local.funcs = arguments.objMetaData.functions;
|
|
for (local.f = 1; local.f <= arrayLen(local.funcs); local.f++) {
|
|
// for every function whose name starts with "getFrom" *and* has a taffy_mime metadata attribute, count it
|
|
local.mime = "";
|
|
if (structKeyExists(local.funcs[local.f], "taffy_mime")) {
|
|
local.mime = local.funcs[local.f].taffy_mime;
|
|
} else if (structKeyExists(local.funcs[local.f], "taffy:mime")) {
|
|
local.mime = local.funcs[local.f]["taffy:mime"];
|
|
}
|
|
if (ucase(left(local.funcs[local.f].name, 7)) == "GETFROM" && len(local.mime)) {
|
|
for (local.m in listToArray(local.mime, ",;")) {
|
|
local.response[local.m] = local.funcs[local.f].name;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return local.response;
|
|
}
|
|
|
|
private struct function buildRequestArguments(required string regex, required array tokenNamesArray, required string uri, required string queryString, required struct headers) output="false" hint="regex that describes the request (including uri and query string parameters)" {
|
|
var local = {};
|
|
var tmp = "";
|
|
local.returnData = {}; // this will be used as an argumentCollection for the method that ultimately gets called
|
|
|
|
// parse path_info data into key-value pairs
|
|
local.tokenValues = reFindNoSuck(arguments.regex, arguments.uri);
|
|
local.numTokenValues = arrayLen(local.tokenValues);
|
|
local.numTokenNames = arrayLen(arguments.tokenNamesArray);
|
|
if (local.numTokenNames > 0) {
|
|
for (local.t = 1; local.t <= local.numTokenNames; local.t++) {
|
|
local.returnData[arguments.tokenNamesArray[local.t]] = urlDecode(local.tokenValues[local.t]);
|
|
}
|
|
}
|
|
// query_string input is also key-value pairs
|
|
for (local.t in listToArray(arguments.queryString, "&")) {
|
|
local.qsKey = urlDecode(listFirst(local.t, "="));
|
|
local.qsValue = "";
|
|
if (listLen(local.t, "=") == 2) {
|
|
local.qsValue = urlDecode(listLast(local.t, "="));
|
|
}
|
|
if ((len(local.qsKey) > 2) && (right(local.qsKey, 2) == "[]")) {
|
|
local.qsKey = left(local.qsKey, len(local.qsKey) - 2);
|
|
if (!structKeyExists(local.returnData, local.qsKey)) {
|
|
local.returnData[local.qsKey] = [];
|
|
}
|
|
arrayAppend(local.returnData[local.qsKey], local.qsValue);
|
|
} else {
|
|
local.returnData[local.qsKey] = local.qsValue;
|
|
}
|
|
}
|
|
// if a mime type is requested as part of the url ("whatever.json"), then extract that so taffy can use it
|
|
if (local.numTokenValues > local.numTokenNames) {
|
|
// when there is 1 more token value than name, that value (regex capture group) is the format
|
|
local.mime = local.tokenValues[local.numTokenValues];
|
|
local.returnData["_taffy_mime"] = local.mime;
|
|
} else if (structKeyExists(arguments.headers, "Accept")) {
|
|
local.headerMatch = false;
|
|
for (tmp in listToArray(arguments.headers.accept)) {
|
|
// deal with that q=0 stuff (just ignore it)
|
|
if (listLen(tmp, ";") > 1) {
|
|
tmp = listFirst(tmp, ";");
|
|
}
|
|
if (structKeyExists(application._taffy.settings.mimeTypes, tmp)) {
|
|
local.returnData["_taffy_mime"] = application._taffy.settings.mimeTypes[tmp];
|
|
local.headerMatch = true;
|
|
break; // exit loop
|
|
} else if (trim(tmp) == "*/*") {
|
|
local.returnData["_taffy_mime"] = application._taffy.settings.defaultMime;
|
|
local.headerMatch = true;
|
|
break; // exit loop
|
|
}
|
|
}
|
|
// if a header is passed, but it didn't match any known mimes, and no mime was found via extension, just use whatever's in the header
|
|
if (local.headerMatch == false) {
|
|
local.returnData["_taffy_mime"] = listFirst(listFirst(arguments.headers.accept, ","), ";");
|
|
}
|
|
}
|
|
return local.returnData;
|
|
}
|
|
|
|
private string function guessResourcesPath() output="false" hint="used to try and figure out the absolute path of the /resources folder even though this file may not be in the web root" {
|
|
var local = {};
|
|
// if /resources has been explicitly defined in an server/application mapping, it should take precedence
|
|
if (directoryExists(expandPath("resources"))) {
|
|
return "resources";
|
|
} else if (directoryExists(expandPath("/resources"))) {
|
|
return "/resources";
|
|
}
|
|
|
|
// if all else fails, fall through to guessing where /resources lives
|
|
local.indexcfmpath = cgi.script_name;
|
|
local.resourcesPath = listDeleteAt(local.indexcfmpath, listLen(local.indexcfmpath, "/"), "/") & "/resources";
|
|
|
|
if (GetContextRoot() != "") {
|
|
local.resourcesPath = ReReplace(local.resourcesPath, "^#GetContextRoot()#", "");
|
|
}
|
|
|
|
return local.resourcesPath;
|
|
}
|
|
|
|
private string function guessResourcesFullPath(string dottedPath = "") output="false" hint="dotted path to a resource folder to use" {
|
|
// when we have an explicit path, no need to make a guess, we just need to convert the dotted path to a file path
|
|
if (len(arguments.dottedPath)) {
|
|
return expandPath("/" & replace(arguments.dottedPath, ".", "/", "all"));
|
|
}
|
|
|
|
return expandPath(guessResourcesPath());
|
|
}
|
|
|
|
private string function guessResourcesCFCPath(string dottedPath = "") output="false" hint="dotted path to a resource folder to use" {
|
|
var path = "";
|
|
// when we have an explicit path, no need to make a guess
|
|
if (len(arguments.dottedPath)) {
|
|
return arguments.dottedPath;
|
|
}
|
|
|
|
path = guessResourcesPath();
|
|
if (left(path, 1) == "/") {
|
|
path = right(path, len(path) - 1);
|
|
}
|
|
return reReplace(path, "\/", ".", "all");
|
|
}
|
|
|
|
private void function throwError(numeric statusCode = 500, required string msg, struct headers = {}) output="false" hint="message to return to api consumer" {
|
|
cfcontent(reset=true);
|
|
addHeaders(arguments.headers);
|
|
cfheader(statuscode=arguments.statusCode, statustext=arguments.msg);
|
|
abort;
|
|
}
|
|
|
|
private array function splitURIs(required string input) output="false" {
|
|
var inBracketCount = 0;
|
|
var output = [];
|
|
var s = createObject("java", "java.lang.StringBuffer").init("");
|
|
var c = "";
|
|
for (c in trim(input).toCharArray()) {
|
|
if (c == "{") {
|
|
inBracketCount += 1;
|
|
} else if (c == "}" && inBracketCount > 0) {
|
|
inBracketCount -= 1;
|
|
}
|
|
if (c == "," && inBracketCount == 0) {
|
|
arrayAppend(output, s.toString());
|
|
s.setLength(0);
|
|
} else {
|
|
s.append(c);
|
|
}
|
|
}
|
|
if (s.length() > 0) {
|
|
arrayAppend(output, s.toString());
|
|
}
|
|
return output;
|
|
}
|
|
|
|
private void function throwExceptionIfURIDoesntBeginWithForwardSlash(required string uri, required string beanName) output="false" {
|
|
var uriDoesntBeginWithForwardSlash = left(arguments.uri, 1) != "/";
|
|
|
|
if (uriDoesntBeginWithForwardSlash) {
|
|
throw(
|
|
message="URI doesn't begin with a forward slash.",
|
|
detail="The URI (#arguments.uri#) for `#arguments.beanName#` should begin with a forward slash.",
|
|
errorcode="taffy.resources.URIDoesntBeginWithForwardSlash"
|
|
);
|
|
}
|
|
}
|
|
|
|
private function cacheBeanMetaData(required factory, required string beanList, required any taffyRef) output="false" {
|
|
var local = {};
|
|
local.endpoints = {};
|
|
for (local.beanName in listToArray(arguments.beanList)) {
|
|
// get the cfc metadata that defines the uri for that cfc
|
|
local.cfcMetadata = getMetaData(arguments.factory.getBean(local.beanName));
|
|
local.uriAttr = "";
|
|
if (structKeyExists(local.cfcMetadata, "taffy_uri")) {
|
|
local.uriAttr = local.cfcMetadata["taffy_uri"];
|
|
} else if (structKeyExists(local.cfcMetadata, "taffy:uri")) {
|
|
local.uriAttr = local.cfcMetadata["taffy:uri"];
|
|
}
|
|
|
|
local.uris = splitURIs(local.uriAttr);
|
|
|
|
if (structKeyExists(local.cfcMetaData, "taffy:aopbean")) {
|
|
local.cachedBeanName = local.cfcMetaData["taffy:aopbean"];
|
|
} else if (structKeyExists(local.cfcMetaData, "taffy_aopbean")) {
|
|
local.cachedBeanName = local.cfcMetaData["taffy_aopbean"];
|
|
} else {
|
|
local.cachedBeanName = local.beanName;
|
|
}
|
|
|
|
// if it doesn't have any uris, then it's not a resource
|
|
if (arrayLen(local.uris)) {
|
|
for (local.uri in local.uris) {
|
|
try {
|
|
local.uri = trim(local.uri);
|
|
local.metaInfo = convertURItoRegex(local.uri);
|
|
if (structKeyExists(local.endpoints, local.metaInfo.uriRegex)) {
|
|
throw(
|
|
message="Duplicate URI scheme detected. All URIs must be unique (excluding tokens).",
|
|
detail="The URI (#local.uri#) for `#local.beanName#` conflicts with the existing URI definition of `#local.endpoints[local.metaInfo.uriRegex].beanName#`",
|
|
errorcode="taffy.resources.DuplicateUriPattern"
|
|
);
|
|
}
|
|
|
|
throwExceptionIfURIDoesntBeginWithForwardSlash(local.uri, local.beanName);
|
|
|
|
local.endpoints[local.metaInfo.uriRegex] = { beanName = local.cachedBeanName, tokens = local.metaInfo.tokens, methods = {}, srcURI = local.uri };
|
|
if (structKeyExists(local.cfcMetadata, "functions")) {
|
|
for (local.f in local.cfcMetadata.functions) {
|
|
if (local.f.name == "get" || local.f.name == "post" || local.f.name == "put" || local.f.name == "patch" || local.f.name == "delete" || local.f.name == "head" || local.f.name == "options") {
|
|
local.endpoints[local.metaInfo.uriRegex].methods[local.f.name] = local.f.name;
|
|
|
|
// also support future/misc verbs via metadata
|
|
} else if (structKeyExists(local.f, "taffy:verb")) {
|
|
local.endpoints[local.metaInfo.uriRegex].methods[local.f["taffy:verb"]] = local.f.name;
|
|
} else if (structKeyExists(local.f, "taffy_verb")) {
|
|
local.endpoints[local.metaInfo.uriRegex].methods[local.f["taffy_verb"]] = local.f.name;
|
|
}
|
|
|
|
// cache any extra function metadata for use in onTaffyRequest
|
|
local.extraMetadata = duplicate(local.f);
|
|
structDelete(local.extraMetadata, "name");
|
|
structDelete(local.extraMetadata, "parameters");
|
|
structDelete(local.extraMetadata, "hint");
|
|
param name="local.endpoints['#local.metaInfo.uriRegex#'].metadata" default={};
|
|
if (!structIsEmpty(local.extraMetadata)) {
|
|
local.endpoints[local.metaInfo.uriRegex].metadata[local.f.name] = local.extraMetadata;
|
|
} else {
|
|
local.endpoints[local.metaInfo.uriRegex].metadata[local.f.name] = {};
|
|
}
|
|
}
|
|
}
|
|
} catch (any cfcatch) {
|
|
// skip cfc's with errors, but save info about them for display in the dashboard
|
|
local.err = {};
|
|
local.err.resource = local.beanName;
|
|
local.err.exception = cfcatch;
|
|
arrayAppend(arguments.taffyRef.status.skippedResources, local.err);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return local.endpoints;
|
|
}
|
|
|
|
private string function getBeanListFromExternalFactory(required bf) output="false" {
|
|
var local = {};
|
|
var beanFactoryMeta = getMetadata(arguments.bf);
|
|
if (lcase(left(beanFactoryMeta.name, 10)) == "coldspring") {
|
|
return getBeanListFromColdSpring(arguments.bf);
|
|
} else if (beanFactoryMeta.name contains "ioc") {
|
|
// this isn't a perfect test (contains "ioc") but it's all we can do for now...
|
|
local.beanInfo = arguments.bf.getBeanInfo().beanInfo;
|
|
local.beanList = "";
|
|
for (local.beanName in local.beanInfo) {
|
|
if (
|
|
structKeyExists(local.beanInfo[local.beanName], "name")
|
|
&& local.beanName != local.beanInfo[local.beanName].name
|
|
&& isInstanceOf(arguments.bf.getBean(local.beanName), "taffy.core.resource")
|
|
) {
|
|
local.beanList = listAppend(local.beanList, local.beanName);
|
|
}
|
|
}
|
|
return local.beanList;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
private string function getBeanListFromColdSpring(required bf) output="false" {
|
|
var local = {};
|
|
local.beans = arguments.bf.getBeanDefinitionList();
|
|
local.beanList = "";
|
|
for (local.beanName in local.beans) {
|
|
/*
|
|
Can't call instanceOf() method on beans generated by factories; there is no
|
|
class property on factory-generated beans, and it throws an error in ColdSpring 1.2.
|
|
Temporary patch is to skip those beans, but we'd really either want to examine the
|
|
return type metadata on the factory method or find some other way to determine if
|
|
this is a Taffy bean.
|
|
*/
|
|
if (local.beans[local.beanName].getBeanClass() != "") {
|
|
if (local.beans[local.beanName].instanceOf("taffy.core.resource")) {
|
|
local.beanList = listAppend(local.beanList, local.beanName);
|
|
}
|
|
}
|
|
}
|
|
return local.beanList;
|
|
}
|
|
|
|
private function inspectMimeTypes(required string customClassDotPath, required factory) output="false" hint="dot-notation path of representation class" {
|
|
if (arguments.factory.containsBean(arguments.customClassDotPath)) {
|
|
return _recurse_inspectMimeTypes(getMetadata(arguments.factory.getBean(arguments.customClassDotPath)));
|
|
} else {
|
|
return _recurse_inspectMimeTypes(getComponentMetadata(arguments.customClassDotPath));
|
|
}
|
|
}
|
|
|
|
private function _recurse_inspectMimeTypes(required struct objMetaData, struct data = {}) output="false" {
|
|
var local = {};
|
|
local.ext = "";
|
|
param name="arguments.data.mimeTypes" default={};
|
|
param name="arguments.data.mimeExtensions" default={};
|
|
param name="arguments.data.defaultMime" default="";
|
|
// recurse into parents first so that child defaults override parent defaults
|
|
if (structKeyExists(arguments.objMetaData, "extends")) {
|
|
arguments.data = _recurse_inspectMimeTypes(arguments.objMetaData.extends, arguments.data);
|
|
}
|
|
// then handle child settings
|
|
if (structKeyExists(arguments.objMetaData, "functions") && isArray(arguments.objMetaData.functions)) {
|
|
local.funcs = arguments.objMetaData.functions;
|
|
for (local.f = 1; local.f <= arrayLen(local.funcs); local.f++) {
|
|
// for every function whose name starts with "getAs" *and* has a taffy_mime metadata attribute, register the mime type
|
|
local.mime = "";
|
|
if (structKeyExists(local.funcs[local.f], "taffy_mime")) {
|
|
local.mime = local.funcs[local.f].taffy_mime;
|
|
} else if (structKeyExists(local.funcs[local.f], "taffy:mime")) {
|
|
local.mime = local.funcs[local.f]["taffy:mime"];
|
|
}
|
|
if (ucase(left(local.funcs[local.f].name, 5)) == "GETAS" && len(local.mime)) {
|
|
local.ext = lcase(right(local.funcs[local.f].name, len(local.funcs[local.f].name) - 5));
|
|
local.mime = lcase(local.mime);
|
|
for (local.thisMime in listToArray(local.mime, ",;")) {
|
|
param name="arguments.data.mimeExtensions['#local.ext#']" default=local.thisMime;
|
|
arguments.data.mimeTypes[local.thisMime] = local.ext;
|
|
}
|
|
// check for taffy_default metadata to set the current mime as the default
|
|
if (structKeyExists(local.funcs[local.f], "taffy_default") && local.funcs[local.f].taffy_default) {
|
|
arguments.data.defaultMime = local.ext;
|
|
} else if (structKeyExists(local.funcs[local.f], "taffy:default") && local.funcs[local.f]["taffy:default"] == true) {
|
|
arguments.data.defaultMime = local.ext;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return arguments.data;
|
|
}
|
|
|
|
private boolean function isSerializerInstance(required any obj) output="false" {
|
|
if (!isObject(arguments.obj)) return false;
|
|
var meta = getMetadata(arguments.obj);
|
|
while (structKeyExists(meta, "name")) {
|
|
if (listLast(meta.name, ".") == "baseSerializer") return true;
|
|
if (!structKeyExists(meta, "extends")) break;
|
|
meta = meta.extends;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private boolean function mimeSupported(required string mimeExt) output="false" {
|
|
if (structKeyExists(application._taffy.settings.mimeExtensions, arguments.mimeExt)) {
|
|
return true;
|
|
}
|
|
if (structKeyExists(application._taffy.settings.mimeTypes, arguments.mimeExt)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private function getReturnMimeAsHeader(required string mimeExt) output="false" {
|
|
if (structKeyExists(application._taffy.settings.mimeExtensions, arguments.mimeExt)) {
|
|
return application._taffy.settings.mimeExtensions[arguments.mimeExt];
|
|
}
|
|
if (structKeyExists(application._taffy.settings.mimeTypes, arguments.mimeExt)) {
|
|
return arguments.mimeExt;
|
|
}
|
|
}
|
|
|
|
private function getReturnMimeAsExt(required string mimeExt) output="false" {
|
|
if (structKeyExists(application._taffy.settings.mimeExtensions, arguments.mimeExt)) {
|
|
return arguments.mimeExt;
|
|
}
|
|
if (structKeyExists(application._taffy.settings.mimeTypes, arguments.mimeExt)) {
|
|
return application._taffy.settings.mimeTypes[arguments.mimeExt];
|
|
}
|
|
}
|
|
|
|
private void function handleDashboardRequest() {
|
|
if (structKeyExists(url, "openapi") || structKeyExists(url, "swagger")) {
|
|
writeOpenApiSpec();
|
|
abort;
|
|
}
|
|
if (!application._taffy.settings.disableDashboard) {
|
|
if (structKeyExists(url, "docs")) {
|
|
include "#application._taffy.settings.docsPath#";
|
|
} else {
|
|
include "../dashboard/dashboard.cfm";
|
|
}
|
|
abort;
|
|
} else {
|
|
if (len(application._taffy.settings.disabledDashboardRedirect)) {
|
|
location(url=application._taffy.settings.disabledDashboardRedirect, addtoken=false);
|
|
abort;
|
|
} else if (application._taffy.settings.showDocsWhenDashboardDisabled) {
|
|
include "#application._taffy.settings.docsPath#";
|
|
abort;
|
|
} else {
|
|
throwError(403, "Forbidden");
|
|
}
|
|
}
|
|
}
|
|
|
|
private void function writeOpenApiSpec() {
|
|
var openapiCfg = application._taffy.settings.openapi;
|
|
if (structKeyExists(openapiCfg, "enabled") && openapiCfg.enabled == false) {
|
|
throwError(403, "Forbidden");
|
|
}
|
|
// cache the serialized spec — it's deterministic from application._taffy and only changes on framework reload,
|
|
// which replaces application._taffy wholesale (setupFramework), naturally invalidating this
|
|
if (!structKeyExists(application._taffy, "openapiSpec")) {
|
|
var generator = createObject("component", "taffy.core.openapi");
|
|
application._taffy.openapiSpec = serializeJson(generator.generate());
|
|
}
|
|
cfcontent(reset=true, type="application/json; charset=utf-8");
|
|
cfheader(name="Access-Control-Allow-Origin", value="*");
|
|
writeOutput(application._taffy.openapiSpec);
|
|
}
|
|
|
|
private boolean function isUnhandledPathRequest(targetPath) {
|
|
return REFindNoCase("^(" & application._taffy.settings.unhandledPathsRegex & ")", arguments.targetPath);
|
|
}
|
|
|
|
private function reFindNoSuck(required string pattern, required string data, startPos = 1) output="false" hint="I wrote this wrapper for reFindNoCase because the way it returns matches is god awful." {
|
|
var local = {};
|
|
local.awesome = [];
|
|
local.sucky = refindNoCase(arguments.pattern, arguments.data, arguments.startPos, true);
|
|
if (!isArray(local.sucky.len) || arrayLen(local.sucky.len) == 0) {
|
|
return [];
|
|
} // handle no match at all
|
|
for (local.i = 1; local.i <= arrayLen(local.sucky.len); local.i++) {
|
|
// if there's a match with pos 0 & length 0, that means the mime type was not specified
|
|
if (local.sucky.len[local.i] > 0 && local.sucky.pos[local.i] > 0) {
|
|
// don't include the group that matches the entire pattern
|
|
local.matchBody = mid(arguments.data, local.sucky.pos[local.i], local.sucky.len[local.i]);
|
|
if (local.matchBody != arguments.data) {
|
|
arrayAppend(local.awesome, local.matchBody);
|
|
}
|
|
}
|
|
}
|
|
return local.awesome;
|
|
}
|
|
|
|
// helper methods: stuff used in Application.cfc
|
|
private function getBeanFactory() output="false" {
|
|
return application._taffy.factory;
|
|
}
|
|
|
|
private function getExternalBeanFactory() output="false" {
|
|
return application._taffy.externalBeanFactory;
|
|
}
|
|
|
|
private function newRepresentation() output="false" hint="private as of 3.0" {
|
|
var repClass = application._taffy.settings.serializer;
|
|
var rep = "";
|
|
if (application._taffy.factory.containsBean(repClass)) {
|
|
rep = application._taffy.factory.getBean(repClass);
|
|
} else {
|
|
rep = createObject("component", repClass);
|
|
}
|
|
rep.setNoDataSends204NoContent(application._taffy.settings.noDataSends204NoContent);
|
|
return rep;
|
|
}
|
|
|
|
public function noData() output="false" {
|
|
return newRepresentation().noData();
|
|
}
|
|
|
|
public function noContent() output="false" {
|
|
return newRepresentation().noContent();
|
|
}
|
|
|
|
public function representationOf(required data) output="false" {
|
|
return newRepresentation().setData(arguments.data);
|
|
}
|
|
|
|
public function rep(required data) output="false" hint="alias for representationOf" {
|
|
return representationOf(arguments.data);
|
|
}
|
|
|
|
private struct function getGlobalHeaders() output="false" {
|
|
return application._taffy.settings.globalHeaders;
|
|
}
|
|
|
|
public string function getPath() output="false" hint="This method returns just the URI portion of the URL, and makes it easier to port Taffy to other platforms by subclassing this method to match the way the platform works. The default behavior is tested and works on Adobe ColdFusion 9.0.1." {
|
|
if (cgi.path_info == cgi.script_name) {
|
|
// WTF! I've only seen this on Win+IIS, seems fine on OSX+Apache...
|
|
return "";
|
|
}
|
|
return cgi.path_info;
|
|
}
|
|
|
|
public void function addHeaders(required struct headers) output="false" {
|
|
var h = "";
|
|
if (!structIsEmpty(arguments.headers)) {
|
|
for (h in arguments.headers) {
|
|
cfheader(name=h, value=arguments.headers[h]);
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct function getBasicAuthCredentials() output="false" {
|
|
var local = {};
|
|
local.credentials = {};
|
|
local.credentials.username = "";
|
|
local.credentials.password = "";
|
|
try {
|
|
local.encodedCredentials = ListLast(GetPageContext().getRequest().getHeader("Authorization"), " ");
|
|
local.decodedCredentials = toString(toBinary(local.EncodedCredentials), "iso-8859-1");
|
|
local.credentials.username = listFirst(local.decodedCredentials, ":");
|
|
local.credentials.password = listRest(local.decodedCredentials, ":");
|
|
} catch (any e) {
|
|
}
|
|
return local.credentials;
|
|
}
|
|
|
|
function getHostname() {
|
|
// unceremoniously stolen from FW/1
|
|
return createObject("java", "java.net.InetAddress").getLocalHost().getHostName();
|
|
}
|
|
|
|
function getHintsFromMetaData(required struct metadata) output="false" {
|
|
var result = {};
|
|
var func = "";
|
|
var f = 0;
|
|
var g = 0;
|
|
var foundFunc = false;
|
|
result.functions = [];
|
|
|
|
// don't recurse if we've reached the base component
|
|
if (structKeyExists(metadata, "extends") && metadata.extends.fullname != "taffy.core.resource") {
|
|
result = getHintsFromMetaData(metadata.extends);
|
|
}
|
|
|
|
// component attributes
|
|
if (structKeyExists(arguments.metadata, "hint")) {
|
|
// intentionally overwrite hint from any parent cfc's
|
|
result.hint = arguments.metadata.hint;
|
|
}
|
|
|
|
// get uri
|
|
if (structKeyExists(arguments.metadata, "taffy_uri")) {
|
|
result.uri = arguments.metadata.taffy_uri;
|
|
}
|
|
if (structKeyExists(arguments.metadata, "taffy:uri")) {
|
|
result.uri = arguments.metadata["taffy:uri"];
|
|
}
|
|
|
|
// if there aren't any functions here to grab, return what we already have
|
|
if (!structKeyExists(metadata, "functions") || !isArray(metadata.functions) || !arrayLen(metadata.functions)) {
|
|
return result;
|
|
}
|
|
|
|
for (f = 1; f <= arrayLen(metadata.functions); f++) {
|
|
func = metadata.functions[f];
|
|
// ignore hidden methods, if access is not set, assume public
|
|
if (!structKeyExists(func, "access") || (func.access != "private" && func.access != "package")) {
|
|
// check to see if this function is already in the list. If so, overwrite, otherwise append
|
|
foundFunc = false;
|
|
for (g = 1; g <= arrayLen(result.functions); g++) {
|
|
if (result.functions[g].NAME == func.NAME) {
|
|
result.functions[g] = func;
|
|
foundFunc = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!foundFunc) {
|
|
arrayAppend(result.functions, func);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function addTaffyHeader(required string name, required string value) {
|
|
if (application._taffy.settings.exposeTaffyHeaders) {
|
|
cfheader(name=arguments.name, value=arguments.value);
|
|
}
|
|
}
|
|
|
|
}
|