Files
svc-api-x/v1/Application.cfc
T
2025-04-08 19:40:03 +03:00

862 lines
39 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<cfcomponent extends="taffy.core.api">
<!---<cfset this.name = "deck_api" />--->
<cfset this.mappings["/resources"] = expandPath("./resources")/>
<cfset this.mappings["/taffy"] = expandPath("./taffy")/>
<cfset this.mappings["/lib"] = expandPath("./lib")/>
<cflock scope="application" type="exclusive" timeout="3"><!---*** не уверен, что это необходимо--->
<cftry>
<!---production environment--->
<cfinclude template="../../conf/prod.cfm"/>
<cfcatch type="ANY">
<cftry>
<!---staging environment--->
<cfinclude template="../../conf/stage.cfm"/>
<cfcatch type="ANY">
<cftry>
<!--- development environment --->
<cfinclude template="../../conf/dev.cfm"/>
<cfcatch type="ANY">
<!---configure right here--->
<!---для ясности--->
<cfset this.config.environment = "dev-default" />
<cfset this.config.datasource = "cmdb" />
<cfset this.config.is_dev_default = "true" /><!---это виднее в сообщении об ошибке--->
</cfcatch>
</cftry>
</cfcatch>
</cftry>
</cfcatch>
</cftry>
<cfset this.datasource = this.config.datasource />
<cfset request.config = this.config />
</cflock>
<cfscript>
this.name = hash(getCurrentTemplatePath());
//variables.framework = structNew(); //
variables.framework.debugKey = "debug";
variables.framework.reloadKey = "reload";
variables.framework.reloadPassword = "true";
variables.framework.reloadOnEveryRequest = true;
variables.framework.serializer = "taffy.core.nativeJsonSerializer";
//variables.framework.serializer = "JsonUtilSerializer";
variables.framework.dashboardKey = "dashboard";
variables.framework.disableDashboard = false;
variables.framework.unhandledPaths = "/flex2gateway";
variables.framework.allowCrossDomain = true; // соответствующие заголовки, включая Access-Control-Allow-Origin выставляются только при наличии заголовка origin в реквесте
//variables.framework.allowCrossDomain = ""; // перепишем явно, возвращая Origin
//variables.framework.docs={};
variables.framework.docs.APIName="Deck API";
variables.framework.docs.APIVersion="0.087";
variables.framework.globalHeaders = structNew();
variables.framework.globalHeaders["Access-Control-Expose-Headers"] = "Location";
variables.framework.globalHeaders["Access-Control-Allow-Credentials"] = "true";
variables.framework.globalHeaders["X-Application-Version"] = variables.framework.docs.APIVersion;
//variables.framework.globalHeaders["Access-Control-Allow-Origin"] = "*";
request.UNDEFINED_USR_ID=-1;
request.ANONYMOUS_USR_ID=2;
request.GUEST_USR_ID=3
////////////////////////////////////////////////////////
request.ORCHESTRATOR_AUTH = createObject("java", "java.lang.System").getEnv("ORCHESTRATOR_AUTH");
if (isNull(request.ORCHESTRATOR_AUTH)) {
request.ORCHESTRATOR_AUTH = "Basic c3ZjX2xrdGVzdDoxMTgzZDIyNzc1OWExYjFkZmVjM2M3NTFiMzIyNzdiYTQ3";//*** времянка, дубль из deck/Application.cfm
}
//*** почему-то scope this ведет себя совершенно не так, как request и variables
// в нем, когда переменная получает значение Empty:null, можно выполнить writedump, а в других получаем ошибку, что такой переменной нет
request.locateIamService=locateIamService; //for possible debug
//request.IAM_SERVICE_URL=this.iamServiceUrl;
</cfscript>
<!--- <cfset this.orchestrator_authorization_string=createObject("java", "java.lang.System").getEnv("ORCHESTRATOR_AUTH")/>
<cfif isEmpty(this.orchestrator_authorization_string)>
<cfset this.orchestrator_authorization_string = "Basic c3ZjX2xrdGVzdDoxMTgzZDIyNzc1OWExYjFkZmVjM2M3NTFiMzIyNzdiYTQ3"/><!--- *** remove --->
</cfif> --->
<cffunction name="rethrow" returntype="void">
<!--- https://www.raymondcamden.com/2004/03/09/3089633C-9FA0-606B-3F540AE9642A795F --->
<cftry>
<cfcatch>
<cfrethrow/>
</cfcatch>
</cftry>
<cfthrow type="Context validation error" message="RETHROW() called outside TRY-CATCH"/>
</cffunction>
<cffunction name="castToBool" returntype="any">
<!--- приводит поле резалтсета к булевому типу, пригодному для сериализации в json --->
<cfargument name="x"/>
<cfif isNull(arguments.x) OR isEmpty(arguments.x)>
<cfreturn arguments.x/><!--- при сериализации преобразуется в null --->
<cfelse>
<cfreturn (arguments.x NEQ 0)/><!--- при сериализации дает false/true --->
</cfif>
</cffunction>
<cfset request.castToBool=#castToBool#/>
<cffunction name="castToBoolSimple" returntype="any">
<!---
приводит поле резалтсета к булевому типу, пригодному для сериализации в json,
вместо null возвращает false
--->
<cfargument name="x"/>
<cfreturn (arguments.x NEQ 0)/><!--- при сериализации дает false/true --->
</cffunction>
<cfset request.castToBoolSimple=#castToBoolSimple#/>
<!--- почему-то эта функция недоступна onApplicationStart --->
<!--- <cffunction
name="getStand"
access="private"
returntype="string"
output="true">
<cftry>
<cfquery name=qConfig>
select value as stand from config
where name='STAND'
</cfquery>
<cfreturn qConfig.stand/>
<cfcatch type="ANY">
</cfcatch>
</cftry>
<cfreturn ""/>
</cffunction> --->
<cffunction name="onApplicationStart">
<!--- <cfset application.dbUser = "username" />
<cfset application.dbPass = "password" />--->
<!---<cfset application._taffy.settings.allowCrossDomain = true/>--->
<cfset this.iamServiceUrl=locateIamService()/> <!--- ***при недоступности будет падать все --->
<cfreturn super.onApplicationStart() />
</cffunction>
<cffunction name="locateIamService"><!--- *** архитектурный колхоз --->
<!--- почему-то мы не видим других функций, когда нас вызывают onApplicationStart. С другой стороны, а почему вообще должны вызывать без перезагрузки приложения --->
<cfset var stand=""/>
<cftry>
<cfquery name=qConfig>
select value as stand from config
where name='STAND'
</cfquery>
<cfset stand = qConfig.stand/>
<cfswitch expression=#stand#>
<cfcase value=",dev">
<cfreturn "https://auth-api-dev.ngcloud.ru/api/v1/user"/>
</cfcase>
<cfcase value="test">
<cfreturn "https://auth-api-test.ngcloud.ru/api/v1/user"/>
</cfcase>
<cfcase value="prod">
<cfreturn "https://auth-api.ngcloud.ru/api/v1/user"/>
</cfcase>
<cfdefaultcase><!--- proceed to the legacy locator ---></cfdefaultcase>
</cfswitch>
<cfcatch type="ANY">
<!--- do nothing, proceed to the legacy locator --->
</cfcatch>
</cftry>
<!--- вот если ничего не прописано, попробуем по-старинке --->
<cfloop list="https://auth-api-dev.ngcloud.ru/api/v1/user,https://auth-api-test.ngcloud.ru/api/v1/user,https://auth-api.ngcloud.ru/api/v1/user" index="serviceUrl"><!--- *** начиная с девелоперского --->
<cftry>
<cfhttp url=#serviceUrl# method="OPTIONS" result="resp" timeout=1/>
<cfreturn serviceUrl/>
<cfcatch type="any">
<!--- <cfdump var=#serviceUrl#/>
<cfdump var=#cfcatch#/><cfabort/> --->
</cfcatch>
</cftry>
</cfloop>
<cfthrow message="IAM service unavailable" detail="cannot find IAM"/>
</cffunction>
<!--- <cffunction name="onRequest">
<cfargument name="template" type="string" required="true"/>
<cfset request.startTickCount=getTickCount()/>
<!---<cfheader name="Access-Control-Allow-Origin" value="*"/>--->
<cfreturn super.onRequest(template) />
</cffunction> --->
<cfscript>
function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers){
/* https://docs.taffy.io/#/3.5.0 */
/*
//allow white-listed requests through
if (cfc == "login"){
return true;
}
//otherwise require a device token
if (!structKeyExists(requestArguments, "deviceToken")){
return newRepresentation().noData().withStatus(401, "Authentication Required");
//and make sure it's valid
}else if (!validateToken(requestArguments.deviceToken)){
return newRepresentation().noData().withStatus(403, "Not Authorized");
}
//return representationOf(requestArguments);
//if a token is included, and valid, allow the request to continue
return true;
*/
////////////////////////////////////////////////
// самым грубым образом получаем данные от IDP, не обрабатывая исключения
//if (variables.framework.allowCrossDomain EQ "") {
//corsHeaders(); // *** нужно, чтобы variables.framework.allowCrossDomain="" иначе задвоятся заголовки
//}
//if (UCase(arguments.verb) EQ 'OPTIONS') return newRepresentation().noData().withStatus("204","No Data").withHeaders({"Content-Type":"application/json;charset=utf-8"});
if (uCase(arguments.verb) EQ 'OPTIONS') return true;
if (lCase(arguments.cfc) EQ 'err') return true;
if (lCase(arguments.cfc) EQ 'throw') return true;
var auth="";
if (structKeyExists(headers,"Authorization")) {
auth=headers.Authorization;
} else {
return representationOf("Authorization header expected").withStatus(401);
}
var result="";
try {
var httpService = new http(method = "GET", charset = "utf-8", url = #this.iamServiceUrl#, timeout="5");
httpService.addParam(type = "HEADER", name = "Accept", value = "application/json");
httpService.addParam(type = "HEADER", name = "Authorization", value = "#auth#"); //passthrough
//writedump(this.iamServiceUrl);abort;
var resp = httpService.send();
//if (resp.status_code NEQ 200) throw("IDP response not OK");
//writedump(resp);abort;
var prefix = resp.getPrefix();
if (prefix.status_code NEQ 200) {
//writedump(this.iamServiceUrl);
//writedump(resp);
return representationOf( {"iamServiceUrl"=this.iamServiceUrl, "idpResponse"=resp} ).withStatus(500);
//abort;
//throw("IDP response not OK");
}
result = prefix.filecontent;
//writedump(result);abort;
} catch (e) {
/*if (fileExists("#GetDirectoryFromPath(GetCurrentTemplatePath())#/etc/local-debug")) { //true for local debug without IDP, etc/* does not go to repository
var result='{"accounts":[{"login":"","type":"telegram"}],"avatar":["d2d2b7ac-50af-432b-b7c4-f7d6561e288a"],"company":"ООО «НУБЕС»","companyId":"8ec70ac0-546d-42a7-8cff-339c8fb51a23","contactId":"983967a3-58c2-4cdd-84a5-8b427ccfac82","email":"smishchuk@nubes.ru","externalUser":false,"fio":{"fullName":"Мищук Сергей ","name":"Сергей","secondName":"","surname":"Мищук"},"groupIds":["94bf5be7-52f4-5c13-87c3-513786934685","ab12b6b8-0265-4683-a5c2-5e1a74a55216","aff008c3-7443-50f6-86e1-ecff3cd3b04d","d6000da0-c9aa-55eb-9882-f118b432730b","d89a33dc-3177-5854-9580-f7f860a5ab7c","ddfe2555-9ef4-42cb-9a2b-7f24e61e7747","df12926f-ecb8-5479-857f-6d291464baad","fda5c295-230a-5025-9797-b8b4e99e08aa","2be9b4b6-94d1-59f9-a649-cab228d82169","64fe6f5c-a91b-5fea-a7ca-d9823701ebd4"],"integration":{"serviceId":""},"login":"smishchuk@nubes.ru","mobilePhone":[],"position":"","userId":"d72530e1-66a4-412f-a046-38153c4e5405"}';}
*/
//writedump(resp);
//return representationOf( {"exception"=e} );
return representationOf( {"exception"=e, "idpResponse"=result} ).withStatus(200);
}
//writeDump(result); abort;
try {
var idpUserData=deserializeJson(result);
arguments.requestArguments.companyUid=idpUserData.companyId;//GUID!
arguments.requestArguments.usrUid=idpUserData.contactId;//userId; //GUID!
} catch (e) {
return representationOf( {"exception"=e, "idpResponse"=result} );
}
var usrCustomerInfo=getUsrCustomerInfo(idpUserData.contactId);
if (lCase(arguments.cfc) EQ 'user') {
if (structIsEmpty(usrCustomerInfo)) return representationOf("User information not found").withStatus(404);
}
if (lCase(arguments.cfc) EQ 'notification_ls') { //*** костыль: для нотификаций можно адресоваться к пользователю без контракта
if (structIsEmpty(usrCustomerInfo)) {
arguments.requestArguments.usrId=-1; //Integer!
arguments.requestArguments.contragentId=-1; //Integer
arguments.requestArguments.contractId=-1; //Integer
arguments.requestArguments.specificationId=-1; //Integer
return true;
}
}
if (structIsEmpty(usrCustomerInfo)) return representationOf("Cannot find default specification for current user #result#").withStatus(422); //это создает довольно много лишних движений при отладке
arguments.requestArguments.usrId=usrCustomerInfo.usrId; //Integer!
arguments.requestArguments.contragentId=usrCustomerInfo.contragentId; //Integer
arguments.requestArguments.contractId=usrCustomerInfo.contractId; //Integer
arguments.requestArguments.specificationId=usrCustomerInfo.specificationId; //Integer
return true;
////////////////////////////////////////////////////////////////////////////
//ensure https because of basic authentication
if (!lcase(request.config.environment) == "development"){
if (!cgi.server_port_secure) { return newRepresentation().noData().withStatus("403.4", "SSL Required");}
}
request.usr_id=request.UNDEFINED_USR_ID;
if (checkAuth()) {return true;}
//return representationOf(arguments.headers);
//return representationOf(arguments.cfc);
var authToken="";
if (structKeyExists(arguments.headers,"x-auth-token")) {authToken=structFind(arguments.headers,"x-auth-token");}
var issuedFor="";
if (structKeyExists(arguments.headers,"x-issued-for")) {issuedFor=structFind(arguments.headers,"x-issued-for");}
if (validatePin(authToken,issuedFor, arguments.verb, arguments.cfc)) return true; //side effect: sets request.usr_id!
if (!validateCredentials(getBasicAuthCredentials())) {
return newRepresentation().noData().withStatus("401", "Invalid Credentials").withHeaders({"WWW-Authenticate":"Basic realm=mms-api"});
}
return true;
}
//private function validateToken(token){ return false; }
//refactoring required
private function validateCredentials(credentials) {//return true;
if (arguments.credentials.username == "medved" /*AND credentials.password == "ghtdtlrhjcfdxtu!Y"*/) {
request.usr_id=-3;
return true;
}
return false;
}
</cfscript>
<!--- стоит ли сообщать отдельно об экспирации пина
нужно блокировать перебор, можно делать пару pin-issuedFor
вероятно, лучше сделать токен девайса
--->
<cffunction name="checkAuth">
<!---https://www.sjoerdlangkemper.nl/2018/09/12/authorization-header-and-cors/--->
<cfset var jwtHelper=CreateObject("component","lib.jwt").init()/>
<cfset var headers=#GetHttpRequestData().headers#/>
<cftry>
<cfset var rawToken=right(headers.Authorization,len(headers.Authorization)-len('bearer '))/>
<cfset var token=jwtHelper.decode(token=rawToken, key=request.config.IDP_certificate.keys[1], algorithms='RS256')/>
<cfset var login=token.preferred_username/>
<!---отрезаем первичный домен --->
<cfset login=ReplaceNoCase(login,"#request.config.auth_domain_suffix#","")/>
<cfquery name="local.qUsr">
select usr_id from usr where login=<cfqueryparam cfsqltype="cf_sql_varchar" value="#login#"/>
</cfquery>
<cfif #local.qUsr.recordCount# EQ 0>
<cfheader statuscode="401" statustext="User not found in local database"/>
<cfreturn false/>
</cfif>
<cfset request.usr_id=local.qUsr.usr_id/>
<cfcatch type="database">
<cfheader statuscode="500" statustext="Internal Server Error - Database Error"/>
<cfoutput>#cfcatch.message# : #cfcatch.detail#</cfoutput>
<cfreturn false/>
</cfcatch>
<cfcatch type="ANY">
<cfif !GetHttpRequestData().method EQ "OPTIONS">
<cfoutput>#cfcatch.message# : #cfcatch.detail#</cfoutput>
<cfreturn false/>
</cfif>
</cfcatch>
</cftry>
<cfreturn true/>
</cffunction>
<cffunction name="getUsrCustomerInfo">
<cfargument name="usrUid"/>
<!--- глупейшее определение спецификации по умолчанию (надо переделать, чтобы создавалась, или вообще просто записывать факты без спеки). Но помним, что при постановке на тестирование цены еше не фиксированы --->
<!--- заметим, что у нас контрагент может быть определен параллельно, от IDP --->
<cfset local={}/>
<!--- Во избежании потери времени при отладке временно сделаем создание дефолтового контракта и спецификации, если их нет --->
<!--- *** хватается первый попавшийся контракт и спецификация --->
<cfquery name="local.qGetCustomerInfo">
select u.usr_id, c.contragent_id, c.contract_id, s.specification_id
from usr u
join contract c on (u.contragent_id=c.contragent_id)
join specification s on (c.contract_id=s.contract_id)
where idp_usr_uid=<cfqueryparam cfsqltype="cf_sql_other" value="#arguments.usrUid#" null=#!isValid('guid',arguments.usrUid)#/>
order by specification_id desc limit 1;
</cfquery>
<!--- <cfdump var=#local.qGetCustomerInfo#/><cfabort/> --->
<cfif local.qGetCustomerInfo.recordCount GT 0>
<cfreturn {"usrId"=#local.qGetCustomerInfo.usr_id#,
"contragentId"=#local.qGetCustomerInfo.specification_id#,
"contractId"=#local.qGetCustomerInfo.specification_id#,
"specificationId"=#local.qGetCustomerInfo.specification_id#
}/>
<cfelse>
<cfreturn {}/> <!--- неуклюжий способ обозначить ошибку --->
</cfif>
</cffunction>
<cffunction name="corsHeaders">
<!--- фрагмент взят из taffy/core/api.cfc и немного переписан --->
<cfset var _taffyRequest=request._taffyRequest/>
<cfset local={}/>
<cfset local.allowVerbs = uCase(structKeyList(_taffyRequest.matchDetails.methods)) />
<cfif true
AND listFindNoCase('PUT,PATCH,DELETE,OPTIONS',_taffyRequest.verb)
AND NOT listFind(local.allowVerbs,'OPTIONS')>
<cfset local.allowVerbs = listAppend(local.allowVerbs,'OPTIONS') />
</cfif>
<cfif structKeyExists(_taffyRequest.headers, "origin") >
<cfheader name="Access-Control-Allow-Origin" value="#_taffyRequest.headers.origin#" />
<cfheader name="Access-Control-Allow-Methods" value="#local.allowVerbs#" />
<!--- Why do we parrot back these headers? See: https://github.com/atuttle/Taffy/issues/144 --->
<cfif not 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" />
<cfelse>
<!--- parrot back all of the request headers to allow the request to continue (can we improve on this?) --->
<cfset local.allowedHeaders = {} />
<cfloop list="Origin,Authorization,X-CSRF-Token,X-Requested-With,Content-Type,X-HTTP-Method-Override,Accept,Referrer,User-Agent" index="local.h">
<cfset local.allowedHeaders[local.h] = 1 />
</cfloop>
<cfset local.requestedHeaders = _taffyRequest.headers['Access-Control-Request-Headers'] />
<cfloop list="#local.requestedHeaders#" index="local.i">
<cfset local.allowedHeaders[ local.i ] = 1 />
</cfloop>
<cfheader name="Access-Control-Allow-Headers" value="#structKeyList(local.allowedHeaders)#" />
</cfif>
</cfif>
</cffunction>
<!--- :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: --->
<!--- :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: --->
<!--- Overriding TAFFY short-circuit logic (taffy/core/api.cfc) --->
<cffunction name="onRequest" output="true" returntype="boolean">
<cfargument name="targetPage" type="string" required="true" />
<cfset request.startTickCount=getTickCount()/><!--- *** --->
<cfset var _taffyRequest = {} />
<cfset var local = {} />
<cfset var m = '' />
<cfset request._taffyRequest = _taffyRequest />
<cfset local.debug = false />
<cfset _taffyRequest.metrics = {} />
<cfset m = _taffyRequest.metrics />
<cfset m.init = getTickCount() />
<!--- enable/disable debug output per settings --->
<cfif not structKeyExists(url, application._taffy.settings.debugKey)>
<cfsetting showdebugoutput="false" />
</cfif>
<!--- display api dashboard if requested --->
<cfif
NOT structKeyExists(url,application._taffy.settings.endpointURLParam)
AND NOT structKeyExists(form,application._taffy.settings.endpointURLParam)
AND len(cgi.path_info) lte 1
AND listFindNoCase(cgi.script_name, "index.cfm", "/") EQ listLen(cgi.script_name, "/")>
<cfif NOT application._taffy.settings.disableDashboard>
<cfif StructKeyExists( URL, "docs" )>
<cfinclude template="#application._taffy.settings.docsPath#" />
<cfelse>
<cfinclude template="../dashboard/dashboard.cfm" />
</cfif>
<cfabort />
<cfelse>
<cfif len(application._taffy.settings.disabledDashboardRedirect)>
<cflocation url="#application._taffy.settings.disabledDashboardRedirect#" addtoken="false" />
<cfabort />
<cfelseif application._taffy.settings.showDocsWhenDashboardDisabled>
<cfinclude template="#application._taffy.settings.docsPath#" />
<cfabort />
<cfelse>
<cfset throwError(403, "Forbidden") />
</cfif>
</cfif>
</cfif>
<!--- get request details --->
<cfset m.beforeParse = getTickCount() />
<cfset local.parsed = parseRequest() />
<cfset m.afterParse = getTickCount() />
<cfset structAppend(_taffyRequest, local.parsed) />
<cfset m.parseTime = m.afterParse - m.beforeParse />
<!--- CORS headers (so that CORS can pass even if the resource throws an exception) --->
<cfset local.allowVerbs = uCase(structKeyList(_taffyRequest.matchDetails.methods)) />
<cfif (application._taffy.settings.allowCrossDomain eq true or len(application._taffy.settings.allowCrossDomain) gt 0)
AND listFindNoCase('PUT,PATCH,DELETE,OPTIONS',_taffyRequest.verb)
AND NOT listFind(local.allowVerbs,'OPTIONS')>
<cfset local.allowVerbs = listAppend(local.allowVerbs,'OPTIONS') />
</cfif>
<cfif structKeyExists(_taffyRequest.headers, "origin") AND (application._taffy.settings.allowCrossDomain eq true or len(application._taffy.settings.allowCrossDomain) gt 0)>
<cfif application._taffy.settings.allowCrossDomain eq true>
<!--- <cfheader name="Access-Control-Allow-Origin" value="*" /> ---><!--- *** dirty hack 2024-10-23 17:59:43--->
<cfheader name="Access-Control-Allow-Origin" value="#_taffyRequest.headers.origin#" />
<cfelse>
<!---
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
--->
<cfset local.domains = listToArray( application._taffy.settings.allowCrossDomain, ', ;' )>
<cfif structKeyExists(_taffyRequest.headers, "origin")>
<cfloop from="1" to="#arrayLen( local.domains )#" index="local.i">
<cfif lcase( rereplace( _taffyRequest.headers.origin, "(http|https):\/\/", "", "all" ) ) EQ 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" />
<cfbreak>
</cfif>
</cfloop>
</cfif>
</cfif>
<cfheader name="Access-Control-Allow-Methods" value="#local.allowVerbs#" />
<!--- Why do we parrot back these headers? See: https://github.com/atuttle/Taffy/issues/144 --->
<cfif not 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" />
<cfelse>
<!--- parrot back all of the request headers to allow the request to continue (can we improve on this?) --->
<cfset local.allowedHeaders = {} />
<cfloop list="Origin,Authorization,X-CSRF-Token,X-Requested-With,Content-Type,X-HTTP-Method-Override,Accept,Referrer,User-Agent" index="local.h">
<cfset local.allowedHeaders[local.h] = 1 />
</cfloop>
<cfset local.requestedHeaders = _taffyRequest.headers['Access-Control-Request-Headers'] />
<cfloop list="#local.requestedHeaders#" index="local.i">
<cfset local.allowedHeaders[ local.i ] = 1 />
</cfloop>
<cfheader name="Access-Control-Allow-Headers" value="#structKeyList(local.allowedHeaders)#" />
</cfif>
</cfif>
<!--- global headers --->
<cfset 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... --->
<cfset m.beforeOnTaffyRequest = getTickCount() />
<cfset _taffyRequest.continue = onTaffyRequest(
_taffyRequest.verb
,_taffyRequest.matchDetails.beanName
,_taffyRequest.requestArguments
,_taffyRequest.returnMimeExt
,_taffyRequest.headers
,_taffyRequest.methodMetadata
,local.parsed.matchDetails.srcUri
) />
<cfset m.afterOnTaffyRequest = getTickCount() />
<cfset m.otrTime = m.afterOnTaffyRequest - m.beforeOnTaffyRequest />
<cfif not structKeyExists(_taffyRequest, "continue")>
<!--- developer forgot to return true --->
<cfthrow
message="Error in your onTaffyRequest method"
detail="Your onTaffyRequest method returned no value. Expected: Return TRUE or call noData()/representationOf()."
errorcode="400"
/>
</cfif>
<cfif isObject(_taffyRequest.continue)>
<!--- inspection complete but request has been aborted by developer; return custom response --->
<cfset _taffyRequest.result = duplicate(_taffyRequest.continue) />
<cfset structDelete(_taffyRequest, "continue")/>
<cfset m.resourceTime = 0 />
<cfelse>
<!--- inspection complete and request allowed by developer --->
<!--- handle requests for simulated responses --->
<cfif structKeyExists(_taffyRequest.requestArguments, application._taffy.settings.simulateKey) and _taffyRequest.requestArguments[application._taffy.settings.simulateKey] eq application._taffy.settings.simulatePassword>
<!--- is there a simulated response? --->
<cfset sampler = 'sample#_taffyRequest.method#Response' />
<cfif structKeyExists(_taffyRequest.matchDetails.metadata, sampler)>
<!--- get simulated response --->
<cfinvoke
component="#application._taffy.factory.getBean(_taffyRequest.matchDetails.beanName)#"
method="#sampler#"
returnvariable="_taffyRequest.result"
/>
<cfset _taffyRequest.result = rep(_taffyRequest.result) />
<cfelse>
<!--- no method for simulated response, so return 400 --->
<cfset _taffyRequest.result = noData().withStatus(400, "No Sample Response Available") />
</cfif>
<cfelse>
<!--- send request to service --->
<cfif structKeyExists(_taffyRequest.matchDetails.methods, _taffyRequest.verb)>
<!--- check the cache before we call the resource --->
<cfset m.cacheCheckTime = getTickCount() />
<cfset local.cacheKey = getCacheKey(
_taffyRequest.matchDetails.beanName
,_taffyRequest.requestArguments
,local.parsed.matchDetails.srcUri
) />
<cfif ucase(_taffyRequest.verb) eq "GET" and validCacheExists(local.cacheKey)>
<cfset m.cacheCheckTime = getTickCount() - m.cacheCheckTime />
<cfset m.cacheGetTime = getTickCount() />
<cfset _taffyRequest.result = getCachedResponse(local.cacheKey) />
<cfset m.cacheGetTime = m.cacheGetTime - getTickCount() />
<cfelse>
<cfif ucase(_taffyRequest.verb) eq "GET">
<cfset m.cacheCheckTime = getTickCount() - m.cacheCheckTime />
<cfelse>
<cfset structDelete(m, "cacheCheckTime") />
</cfif>
<!--- returns a representation-object --->
<cfset m.beforeResource = getTickCount() />
<cfinvoke
component="#application._taffy.factory.getBean(_taffyRequest.matchDetails.beanName)#"
method="#_taffyRequest.method#"
argumentcollection="#_taffyRequest.requestArguments#"
returnvariable="_taffyRequest.result"
/>
<cfset m.afterResource = getTickCount() />
<cfset m.resourceTime = m.afterResource - m.beforeResource />
<cfif !isDefined("_taffyRequest.result")>
<cfthrow
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"
/>
</cfif>
<!--- 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. --->
<cfif !isInstanceOf(_taffyRequest.result, "taffy.core.baseSerializer")>
<cfset _taffyRequest.result = rep(_taffyRequest.result) />
</cfif>
<cfif ucase(_taffyRequest.verb) eq "GET" and structKeyExists(local, "cacheKey")>
<cfset m.cacheSaveStart = getTickCount() />
<cfset setCachedResponse(local.cacheKey, _taffyRequest.result) />
<cfset m.cacheSaveTime = getTickCount() - m.cacheSaveStart />
</cfif>
</cfif>
<cfelseif NOT listFind(local.allowVerbs,_taffyRequest.verb)>
<!--- if the verb is not implemented, refuse the request --->
<cfheader name="ALLOW" value="#local.allowVerbs#" />
<cfset throwError(405, "Method Not Allowed") />
<cfelse>
<!--- create dummy response for cross domain OPTIONS request --->
<cfset _taffyRequest.resultHeaders = structNew() />
<cfset _taffyRequest.statusArgs = structNew() />
<cfset _taffyRequest.statusArgs.statusCode = 200 />
<cfset _taffyRequest.statusArgs.statusText = 'OK' />
</cfif>
</cfif>
</cfif>
<!--- make sure the requested mime type is available --->
<cfif not mimeSupported(_taffyRequest.returnMimeExt)>
<cfset throwError(400, "Requested format not available (#_taffyRequest.returnMimeExt#)") />
</cfif>
<cfif structKeyExists(_taffyRequest,'result')>
<!--- get status code --->
<cfset _taffyRequest.statusArgs = structNew() />
<cfset _taffyRequest.statusArgs.statusCode = _taffyRequest.result.getStatus() />
<cfset _taffyRequest.statusArgs.statusText = _taffyRequest.result.getStatusText() />
<!--- get custom headers --->
<cfinvoke
component="#_taffyRequest.result#"
method="getHeaders"
returnvariable="_taffyRequest.resultHeaders"
/>
</cfif>
<cfsetting enablecfoutputonly="true" />
<cfcontent reset="true" type="#getReturnMimeAsHeader(_taffyRequest.returnMimeExt)#; charset=utf-8" />
<cfheader statuscode="#_taffyRequest.statusArgs.statusCode#" statustext="#_taffyRequest.statusArgs.statusText#" />
<!--- headers --->
<cfset 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 --->
<cfheader name="X-TIME-IN-PARSE" value="#m.parseTime#" />
<cfheader name="X-TIME-IN-ONTAFFYREQUEST" value="#m.otrTime#" />
<cfif structKeyExists(m, "resourceTime")>
<cfheader name="X-TIME-IN-RESOURCE" value="#m.resourceTime#" />
</cfif>
<cfif structKeyExists(m, "cacheCheckTime")>
<cfheader name="X-TIME-IN-CACHE-CHECK" value="#m.cacheCheckTime#" />
</cfif>
<cfif structKeyExists(m, "cacheGetTime")>
<cfheader name="X-TIME-IN-CACHE-GET" value="#m.cacheGetTime#" />
</cfif>
<cfif structKeyExists(m, "cacheSaveTime")>
<cfheader name="X-TIME-IN-CACHE-SAVE" value="#m.cacheSaveTime#" />
</cfif>
<cfif application._taffy.settings.exposeHeaders>
<cfset local.exposeHeaderList = structKeyList(_taffyRequest.resultHeaders) />
<cfset local.exposeHeaderValue = "" />
<cfif application._taffy.settings.useEtags and _taffyRequest.verb eq "GET" and _taffyRequest.result.getType() eq "textual">
<cfset local.exposeHeaderList = listAppend(local.exposeHeaderList, "Etag") />
</cfif>
<cfloop list="#local.exposeHeaderList#" index="local.exposeHeader">
<!--- filter out default simple response headers: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers --->
<cfif not listFindNoCase("Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma", local.exposeHeader)>
<cfset local.exposeHeaderValue = listAppend(local.exposeHeaderValue, local.exposeHeader) />
</cfif>
</cfloop>
<cfif listLen(local.exposeHeaderValue) gt 0>
<cfheader name="Access-Control-Expose-Headers" value="#local.exposeHeaderValue#" />
</cfif>
</cfif>
<!--- result data --->
<cfif structKeyExists(_taffyRequest,'result')>
<cfset _taffyRequest.resultType = _taffyRequest.result.getType() />
<cfset local.resultSerialized = '' />
<cfif _taffyRequest.resultType eq "textual">
<!--- serialize the representation's data into the requested mime type --->
<cfset _taffyRequest.metrics.beforeSerialize = getTickCount() />
<cfinvoke
component="#_taffyRequest.result#"
method="getAs#_taffyRequest.returnMimeExt#"
returnvariable="_taffyRequest.resultSerialized"
/>
<cfset _taffyRequest.metrics.afterSerialize = getTickCount() />
<cfset m.serializeTime = m.afterSerialize - m.beforeSerialize />
<cfheader name="X-TIME-IN-SERIALIZE" value="#m.serializeTime#" />
<!--- apply jsonp wrapper if requested --->
<cfif structKeyExists(_taffyRequest, "jsonpCallback")>
<cfset _taffyRequest.resultSerialized = _taffyRequest.jsonpCallback & "(" & _taffyRequest.resultSerialized & ");" />
</cfif>
<!--- don't return data if etags are enabled and the data hasn't changed --->
<cfif application._taffy.settings.useEtags and _taffyRequest.verb eq "GET">
<!--- etag values are quoted per: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag --->
<cfif structKeyExists(server, "lucee")>
<!--- hashCode() will not work for lucee, see issue #354 --->
<cfset _taffyRequest.serverEtag = '"' & hash(_taffyRequest.resultSerialized) & '"' />
<cfelse>
<cfset _taffyRequest.serverEtag = '"' & _taffyRequest.result.getData().hashCode() & '"' />
</cfif>
<cfif structKeyExists(_taffyRequest.headers, "If-None-Match")>
<cfset _taffyRequest.clientEtag = _taffyRequest.headers['If-None-Match'] />
<cfif len(_taffyRequest.clientEtag) gt 0 and _taffyRequest.clientEtag eq _taffyRequest.serverEtag>
<cfheader statuscode="304" statustext="Not Modified" />
<cfcontent reset="true" type="#application._taffy.settings.mimeExtensions[_taffyRequest.returnMimeExt]#; charset=utf-8" />
<cfreturn true />
<cfelse>
<cfheader name="Etag" value="#_taffyRequest.serverEtag#" />
</cfif>
<cfelse>
<cfheader name="Etag" value="#_taffyRequest.serverEtag#" />
</cfif>
</cfif>
<cfset m.done = getTickCount() />
<cfset m.taffyTime = m.done - m.init - m.parseTime - m.otrTime - m.serializeTime />
<cfif structKeyExists(m, "resourceTime")>
<cfset m.taffyTime -= m.resourceTime />
</cfif>
<cfheader name="X-TIME-IN-TAFFY" value="#m.taffyTime#" />
<cfcontent reset="true" type="#application._taffy.settings.mimeExtensions[_taffyRequest.returnMimeExt]#; charset=utf-8" />
<cfif _taffyRequest.resultSerialized neq ('"' & '"')>
<cfset local.resultSerialized = _taffyRequest.resultSerialized />
</cfif>
<!--- debug output --->
<cfif structKeyExists(url, application._taffy.settings.debugKey)>
<cfset local.debug = true />
</cfif>
<cfelseif _taffyRequest.resultType eq "filename">
<cfset m.done = getTickCount() />
<cfset m.taffyTime = m.done - m.init - m.parseTime - m.otrTime - m.resourceTime />
<cfheader name="X-TIME-IN-TAFFY" value="#m.taffyTime#" />
<cfcontent reset="true" file="#_taffyRequest.result.getFileName()#" type="#_taffyRequest.result.getFileMime()#" deletefile="#_taffyRequest.result.getDeleteFile()#" />
<cfelseif _taffyRequest.resultType eq "filedata">
<cfset m.done = getTickCount() />
<cfset m.taffyTime = m.done - m.init - m.parseTime - m.otrTime - m.resourceTime />
<cfheader name="X-TIME-IN-TAFFY" value="#m.taffyTime#" />
<cfcontent reset="true" variable="#_taffyRequest.result.getFileData()#" type="#_taffyRequest.result.getFileMime()#" />
<cfelseif _taffyRequest.resultType eq "imagedata">
<cfset m.done = getTickCount() />
<cfset m.taffyTime = m.done - m.init - m.parseTime - m.otrTime - m.resourceTime />
<cfheader name="X-TIME-IN-TAFFY" value="#m.taffyTime#" />
<cfcontent reset="true" variable="#_taffyRequest.result.getImageData()#" type="#_taffyRequest.result.getFileMime()#" />
</cfif>
</cfif>
<cfset local.resultSerialized = "" />
<cfif structKeyExists( _taffyRequest, "resultSerialized" )>
<cfset local.resultSerialized = _taffyRequest.resultSerialized />
</cfif>
<cfset local.result = StructNew() />
<cfif structKeyExists( _taffyRequest, "result" )>
<cfset local.result = _taffyRequest.result.getData() />
</cfif>
<!--- ...after the service has finished... --->
<cfset m.beforeOnTaffyRequestEnd = getTickCount() />
<cfset 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
) />
<cfset m.otreTime = getTickCount() - m.beforeOnTaffyRequestEnd />
<cfheader name="X-TIME-IN-ONTAFFYREQUESTEND" value="#m.otreTime#" />
<cfif len(trim(local.resultSerialized))>
<cfoutput>#local.resultSerialized#</cfoutput>
</cfif>
<!--- debug output --->
<cfif local.debug>
<cfoutput><h3>Request Details:</h3><cfdump var="#_taffyRequest#"></cfoutput>
</cfif>
<cfreturn true />
</cffunction>
<!--- <cffunction name="checkForLocalDebug">
<cfreturn fileExists("#GetDirectoryFromPath(GetCurrentTemplatePath())#/etc/local-debug")>
</cffunction> --->
</cfcomponent>