Files
svc-api-x/v1/taffy/core/api.cfc

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);
}
}
}