% /* $Revision$ $Id$ File: api2_rest.inc Description: Functies voor de http-REST interface op de modellen Notes: Hier wordt van alles met de 'buitenwereld' gecommuniceerd. Het manipuleren met behulp van modellen gebeurt in api2.inc Status: */ %> <% var DEZE = this; api2_rest = { authenticate: function _authenticate() { var APIKEY; if (S("fac_api_key_in_url")) APIKEY = getQParam("APIKEY", ""); if (!APIKEY && Request.ServerVariables("HTTP_X_FACILITOR_API_KEY").Count) APIKEY = String(Request.ServerVariables("HTTP_X_FACILITOR_API_KEY")); // Meegegeven als X-FACILITOR-API-Key if (!APIKEY && Session("user_key") > 0) { user_key = Session("user_key"); // Hierdoor is de API intern te gebruiken zonder authenticatie } else { if (Session("user_key") > 0) {} // Tijdens ontwikkeling heb je soms in tweede tab de GUI open. Laat dat ongemoeid. else Session.Abandon(); // Altijd, voor de zekerheid var sql = "SELECT prs_perslid_key, prs_perslid_naam" + " FROM prs_perslid" + " WHERE prs_perslid_verwijder IS NULL" + " AND prs_perslid_apikey = " + safe.quoted_sql(APIKEY); var oRs = Oracle.Execute(sql); if (oRs.Eof || !APIKEY) { Response.Status = "401 Unauthorized"; // Sommige applicaties kunnen in reactie hierop een b64 encoded username:password sturen // Die onderscheppen wij in LoginTry.asp uiteindelijk if (S("basic_auth_realm")) Response.AddHeader("WWW-Authenticate", "Basic realm=\"" + S("basic_auth_realm") + "\""); Response.End; }; __Log("API2 User is: " + oRs("prs_perslid_naam").Value); /* global */ user_key = oRs("prs_perslid_key").Value; if (typeof NO_ADDHEADER == "undefined" && Request.Servervariables("HTTP_FCLT_VERSION").Count > 0) { // wordt opgepikt door FCLTAPI.DLL voor in de logging en daarna gestript. Niet in Fiddler dus Response.AddHeader ("FCLT_USERID", customerId + "\\" + String(user_key)); } oRs.Close(); } // APP? Die kan meta-rechten hebben (bijvoorbeeld auth-token opvragen van gebruiker) var APPKEY; if (S("fac_api_key_in_url")) APPKEY = getQParam("HTTP_X_FACILITOR_APP_KEY", ""); if (!APPKEY && Request.ServerVariables("HTTP_X_FACILITOR_APP_KEY").Count) APPKEY = String(Request.ServerVariables("HTTP_X_FACILITOR_APP_KEY")); // Meegegeven als X-FACILITOR-APP-Key if (APPKEY) { var sql = "SELECT prs_perslid_key, prs_perslid_naam" + " FROM prs_perslid" + " WHERE prs_perslid_verwijder IS NULL" + " AND prs_perslid_apikey = " + safe.quoted_sql(APPKEY); var oRs = Oracle.Execute(sql); if (oRs.Eof) { __DoLog("Unauthorized app"); Response.Status = "401 Unauthorized"; Response.End; }; __Log("APP User is: " + oRs("prs_perslid_naam").Value); /* global */ app_user_key = oRs("prs_perslid_key").Value; /* global */ app_user = new Perslid(app_user_key); oRs.Close() } /* global */ user = new Perslid(user_key); // wordt mogelijk nog overruled door imporsonate CheckForLogging(Request.QueryString("logging")); // Nu pas kan autorisatie via user gecontroleerd worden }, impersonate: function _impersonate(model) { // Impersonate? (anno jan-2016 in de praktijk nergens gebruikt, kan mogelijk vervallen) if (!S("fac_api_allow_impersonate") || !model.impersonate_auth) return; var IMPERS; if (S("fac_api_key_in_url")) IMPERS = getQParam("SWITCHUSER", ""); if (!IMPERS && Request.ServerVariables("HTTP_X_FACILITOR_SWITCH_USER").Count) IMPERS = String(Request.ServerVariables("HTTP_X_FACILITOR_SWITCH_USER")); // Meegegeven als X-FACILITOR-SWITCH-USER if (!IMPERS) return; var sql = "SELECT prs_perslid_key, prs_perslid_naam" + " FROM prs_perslid" + " WHERE prs_perslid_verwijder IS NULL" + " AND prs_perslid_oslogin = " + safe.quoted_sql_upper(IMPERS); var oRs = Oracle.Execute(sql); if (oRs.Eof) { Response.Status = "412 Invalid X-Facilitor-Switch-User header"; Response.End; }; __Log("IMPERS User is: " + oRs("prs_perslid_naam").Value); var other_user_key = oRs("prs_perslid_key").Value; oRs.Close(); var xfunc = user.func_enabled2(model.module, { prs_key: other_user_key, isOptional: true }); var can = (xfunc && xfunc.canRead(model.impersonate_auth)); if (can) { /* global */ user_key = other_user_key; /* global */ user = new Perslid(user_key); } else { Response.Status = "412 Unauthorized X-Facilitor-Switch-User header"; Response.End; } }, process: function _process(model) { var wasCodePage = Session.Codepage; Session.Codepage = 65001; // We doen *uitsluitend* utf-8 Response.Charset = 'utf-8'; api2_rest.authenticate(); // Kip-ei: de omzetting naar new model() mag pas als je geauthenticeerd bent // Hieroboven willen we heb echter al wel meegeven if (typeof model == "function") // Nieuwe stijl is het een function. Even compatible. model = new model(); api2_rest.impersonate(model); var method = String(Request.ServerVariables("REQUEST_METHOD")); if (!/GET|PUT|POST|DELETE/.test(method)) // Overigens houdt IIS deze al eerder tegen { Response.Status = "405 Method not allowed"; Response.End; } if (!("REST_" + method in model)) { Response.Status = "501 Not Implemented"; // TODO The response MUST include an Allow header containing a list of valid methods for the requested resource. Response.End; } var jsondata = {}; var filter = shared.qs2json(model); filter = api2_rest.plugin.transform_filter(filter); var requestparams = { filter: filter, include: getQParamArray("include", []) }; if (/PUT|POST/.test(method)) // Dan is er in de body data meegestuurd { switch (getQParamSafe("format", "invalid").toLowerCase()) { case "json": { var parsed = RequestJSON(); if (parsed.error) api2.error(500, "Error parsing input JSON: " + parsed.error); jsondata = api2_rest.plugin.transform_incoming(requestparams, parsed.json); if (!jsondata) api2.error(500, "Error parsing input JSON: Empty"); break; } case "xml": { var parsed = RequestXML(); if (parsed.error) api2.error(500, "Error parsing input XML: " + parsed.error); jsondata = api2_rest.xml2json(parsed.xml); if (!jsondata) api2.error(500, "Error parsing input XML: Empty"); break; } default: UNKNOWN_FORMAT_TYPE; } if (!jsondata || !(model.record_name in jsondata || (model.multi_update && model.records_name in jsondata))) { api2.error(500, "No '{0}' found in input".format(model.record_name)); } } var key = getQParamInt("id", -1); // Voor POST/PUT/DELETE var isSingle = /PUT|POST|DELETE/.test(method) || (key > 0); // PUT, POST en DELETE altijd single if (getQParamSafe("format", "json") == "doc") { // Dan hoeven we verder niets te doen } else if (getQParamSafe("format", "json") == "api") { // TODO: Onderstaande in een of ander standaardformaat opleveren? var result = { id: model.records_name, methods: [], includes: [], fields: [] }; for (var i in model.includes) result.includes.push(i); if ("REST_GET" in model) result.methods.push("GET"); if ("REST_PUT" in model) result.methods.push("PUT"); if ("REST_POST" in model) result.methods.push("POST"); if ("REST_DELETE" in model) result.methods.push("DELETE"); for (var fld in model.fields) { // TODO: We missen hard-coded filters als reservableequipment/allowedinroom nu nog if (!model.fields.hidden) result.fields.push({ id: fld, filter: model.fields[fld].filter, type: model.fields[fld].typ, label: model.fields[fld].label }); } } else { if (method == "DELETE") { var result = model["REST_" + method]( requestparams, key ); } else if (method == "GET" && filter.mode == "attachment" && "custom_fields" in model.includes) { if (wasCodePage != 65001) { // Door de IIS rewriter is de filenaam in de url utf-8 encoded // Zet dat hier terug om naar Windows-1252 var fileStream = new ActiveXObject("ADODB.Stream"); fileStream.Open(); fileStream.Type = 2; // adTypeText fileStream.Charset = 'Windows-1252'; fileStream.WriteText(filter.filename); fileStream.Position = 0; fileStream.Charset = 'utf-8'; filter.filename = fileStream.ReadText(); fileStream.Close(); } requestparams.filter.id = key; // Die kan er maar beter wel zijn! requestparams.include = ["custom_fields"]; var data = model.REST_GET(requestparams); if (!data.length) // mogelijk not authorized op hele record Response.Status = "404 Not Found"; else model.includes["custom_fields"].model.streamattachment(filter, data[0].custom_fields); Response.End; } else if (method == "GET") { var result = model["REST_" + method]( requestparams, null, key ); } else if (model.record_name in jsondata) // een enkel record { if (jsondata[model.record_name] instanceof Array) abort_with_warning("API2 error: {0} should be single record only.".format(method)); var result = model["REST_" + method]( requestparams, jsondata[model.record_name], key ); } else { // Loop door de multiple records en geef de REST_ functie altijd één record for (var record in jsondata[model.records_name]) { var thisdata = jsondata[model.records_name][record]; var result = model["REST_" + method]( requestparams, thisdata, thisdata.id ); } } } switch (method) { case "DELETE": { Response.Status = "204 No Content"; Response.End; break; } case "GET": { data = result; break; } case "PUT": case "POST": { var key = result.key; if (key > 0) { var params = { filter: shared.qs2json(model), include: getQParamArray("include", []) }, jsondata, key // requestparams.include is mogelijk uitgebreid met wat er in de body stond data = model.REST_GET({ filter: { id: key }, include: requestparams.include }); // resulterende data weer terug __Log(data); } else { data = []; isSingle = false; } } } api2_rest.deliver(data, model, getQParamSafe("format", "json"), isSingle); }, // Data is een array met 'records' deliver: function _deliver(data, model, format, single ) { if (single && !data.length) { Response.Status = "404 Not Found"; Response.End; } if (format == "html" || format == "json") { var result = { }; if (single) result[model.record_name] = data[0]; else { result.total_count = model.total_count; result.limit = model.limit; result.offset = 0; result[model.records_name] = data; } var resultdata = api2_rest.plugin.transform_outgoing({}, result); } switch (format) { case "api": { //var xml_antwoord = api2_rest.json2xml(data, model, single); var str_antwoord = JSON.stringify(data, null, getQParam("pretty","0")=="1"?2:0); Response.ContentType = "application/json"; break; } case "doc": { if (model.fields) // Ga de hints er bij zoeken { var safefieldnames = []; var lcl2fld = {}; for (var fld in model.fields) { var lclname = "{0}.{1}.hint".format(model.records_name, fld); lcl2fld[lclname] = fld; safefieldnames.push(safe.quoted_sql(lclname)); } //model.fields[fld].hint = "Hallo"; var sql = "SELECT fac_locale_xsl_label, " + " COALESCE(fac_locale_xsl_cust, fac_locale_xsl_tekst) fac_locale_xsl_tekst" + " FROM fac_locale_xsl xsl" + " WHERE fac_locale_xsl_lang = " + safe.quoted_sql(user_lang) + " AND fac_locale_xsl_module = 'ASP'" + " AND fac_locale_xsl_label IN (" + safefieldnames.join(", ") + ")"; var oRs = Oracle.Execute(sql); while (!oRs.Eof) { model.fields(lcl2fld[oRs("fac_locale_xsl_label").Value]) = oRs("fac_locale_xsl_tekst").value; oRs.MoveNext(); } } Response.ContentType = "text/html"; var str_antwoord = simple_json2xml(model, "api"); var xml_antwoord = new ActiveXObject("MSXML2.DOMDocument.6.0"); xml_antwoord.loadXML(str_antwoord); if (xml_antwoord.parseError.errorCode) { abort_with_warning("XSL error: " + xml_antwoord.parseError.reason + " @ " + xml_antwoord.parseError.line + "." + xml_antwoord.parseError.linepos + "\n"+ xml_antwoord.parseError.srcText); } var style = new ActiveXObject("MSXML2.DOMDocument.6.0"); style.async = false; style.resolveExternals = false; if (Request.QueryString("debug").Count == 0) { var xslname = model.xslname || "reference.xsl"; style.load(Server.MapPath(rooturl + "/appl/api2/" + xslname)); // De stylesheet laden. API's redeneren vanuit de root var str_antwoord = xml_antwoord.transformNode(style); // terugstoppen in antwoord } else { if (Application("otap_environment") != "O") ONLY_ON_OTAP_O; style.load(Server.MapPath(rooturl + "/appl/shared/indent.xsl")); // De stylesheet laden. API's redeneren vanuit de root var str_antwoord = "
" + Server.HTMLEncode(xml_antwoord.transformNode(style)) + ""; } if (style.parseError.errorCode) { abort_with_warning("XSL error: " + style.parseError.reason + " @ " + style.parseError.line + "." + style.parseError.linepos ); } break; } case "json": var str_antwoord = JSON.stringify(resultdata, null, getQParam("pretty","0")=="1"?2:0); var jsonp = getQParam("jsonp", getQParam("callback","")); if (jsonp) { str_antwoord = jsonp + "(" + str_antwoord + ")"; Response.ContentType = "application/javascript"; } else Response.ContentType = "application/json"; break; case "html": Response.ContentType = "text/html"; var antwoord = JSON.stringify(resultdata, null, 2); var str_antwoord = "
"
+ Server.HTMLEncode(antwoord)
+ "";
break;
case "xml":
Response.ContentType = "text/xml";
var xml_antwoord = api2_rest.json2xml(data, model, single);
// TODO: Output XSL transform ondersteunen?
var xsl = getQParamSafe("xsl", "");
var xslfile;
if (xsl)
{
var fso = new ActiveXObject("Scripting.FileSystemObject");
xslfile = Server.MapPath(custpath + "/xsl/" + xsl + ".xsl");
if (!fso.FileExists(xslfile))
abort_with_warning("Stylesheet '{0}' not found".format(xsl));
}
else if (getQParam("pretty","0")=="1")
xslfile = Server.MapPath(rooturl + "/appl/shared/indent.xsl");
if (xslfile)
{
var style = new ActiveXObject("MSXML2.DOMDocument.6.0");
style.async = false;
style.resolveExternals = false;
style.load(xslfile);
if (style.parseError.errorCode)
{
abort_with_warning("XSL error: " + style.parseError.reason + " @ " + style.parseError.line + "." + style.parseError.linepos );
}
var str_antwoord = xml_antwoord.transformNode(style); // terugstoppen in antwoord
}
else
var str_antwoord = xml_antwoord.xml;
//Response.ContentType = "application/json";
//var str_antwoord = JSON.stringify(api2_rest.xml2json(xml_antwoord), null, 2);;
break;
default:
WRONG_FORMAT;
}
// str_antwoord heeft nu het te versturen antwoord
// Bepaal eTag
var oCrypto = new ActiveXObject("SLNKDWF.Crypto");
var eTag = '"' + oCrypto.hex_sha1(String(S("cache_changecounter")) + "_" + str_antwoord).toLowerCase() + '"';
Response.AddHeader("ETag", eTag);
if (Request.ServerVariables("HTTP_IF_NONE_MATCH") == eTag)
{ // We hebben een match! Effectief besparen wel alleen op dataverkeer, de queries zijn al geweest
Response.Clear();
Response.Status = "304 Not modified";
Response.End;
}
Response.write(str_antwoord);
},
// TODO: Wanneer attributes gebruiken en wanneer (sub)-elements?
// Streven: data == xml2json(json2xml(data))
json2xml: function _json2xml(data, model, single)
{
var rootname = model.records_name;
var record_name = model.record_name;
var xmlDoc = new ActiveXObject("MSXML2.DOMDocument.6.0");
xmlDoc.appendChild(xmlDoc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"utf-8\""));
var record2json = function(record, record_name)
{
var elementRecord = xmlDoc.createElement(record_name);
for (var fld in record)
{
var elementField = xmlDoc.createElement(fld);
if (record[fld] instanceof Date)
{
var elementFieldText = xmlDoc.createTextNode(String(record[fld].toJSON()));
elementField.appendChild(elementFieldText);
}
else if (record[fld] instanceof Array)
{
var inc_record_name;
for (var ii in model.includes)
{
// if (model.includes[ii].model.records_name == fld)
// inc_record_name = model.includes[ii].model.record_name;
if (ii == fld)
inc_record_name = model.includes[ii].model.record_name;
}
// Fallback voor attachments array --> "attachment"
if (!inc_record_name)
inc_record_name = fld.substr(0, fld.length-1);
for (var i = 0; i < record[fld].length; i++)
elementField.appendChild(record2json(record[fld][i], inc_record_name));
}
else if (record[fld] && typeof record[fld] == "object") // Veronderstelt dat dit foreign met name/key is
{
if ("name" in record[fld] && "id" in record[fld])
{
elementField.setAttribute("name", record[fld].name===null?"":record[fld].name);
elementField.setAttribute("id", record[fld].id);
}
else
elementField = record2json(record[fld], fld);
}
else
{
var elementFieldText = xmlDoc.createTextNode(String(record[fld]||""));
elementField.appendChild(elementFieldText);
}
elementRecord.appendChild(elementField);
}
return elementRecord;
};
if (single)
{
xmlDoc.appendChild(record2json(data[0], record_name));
}
else
{
var arrayElement = xmlDoc.createElement(rootname);
for (var i = 0; i < data.length; i++)
arrayElement.appendChild(record2json(data[i], record_name));
xmlDoc.appendChild(arrayElement);
}
return xmlDoc;
},
// Streven: data == json2xml(xml2json(xml))
// http://davidwalsh.name/convert-xml-json maar @attributes er uit gehaald
xml2json: function _xml2json(xml)
{
// Create the return object
var obj = {};
if (xml.nodeType == 1)
{ // element
// do attributes
if (xml.attributes.length > 0)
{
// JGL removed: obj["@attributes"] = {};
for (var j = 0; j < xml.attributes.length; j++)
{
var attribute = xml.attributes.item(j);
obj[attribute.nodeName] = attribute.nodeValue;
}
}
}
else if (xml.nodeType == 3)
{ // text
obj = xml.nodeValue;
}
// do children
if (xml.hasChildNodes())
{
for(var i = 0; i < xml.childNodes.length; i++)
{
var item = xml.childNodes.item(i);
var nodeName = item.nodeName;
if (typeof(obj[nodeName]) == "undefined")
{
// JGL Added: Only one Textnode is simplified. Autodetect data
if (item.nodeType == 3 && xml.childNodes.length == 1)
{
var dt = myJSON.internal_parsedate(null, item.nodeValue);
if (dt && dt instanceof Date)
return dt;
return item.nodeValue;
}
obj[nodeName] = api2_rest.xml2json(item);
}
else
{
if (typeof(obj[nodeName].push) == "undefined")
{
var old = obj[nodeName];
obj[nodeName] = [];
obj[nodeName].push(old);
}
obj[nodeName].push(api2_rest.xml2json(item));
}
}
}
return obj;
},
find_plugin: function()
{
var plugin_name = getQParamSafe("plugin", "").toLowerCase();
if (!plugin_name)
return {};
var fso = new ActiveXObject("Scripting.FileSystemObject");
var paths = ["/cust/" + customerId, "/cust", "/appl/api2"]; // Hieronder zoeken naar '/plugins' folder
for (var p in paths)
{
var ppath = Server.MapPath(rooturl + paths[p] + "/plugins/" + plugin_name + ".wsc")
//__Log(ppath);
if (fso.FileExists(ppath))
{
try
{
var hook = GetObject("script:" + ppath);
}
catch(e)
{
api2.error(500, "Loading {0} failed: {1}".format(ppath, e.description));
}
// Via DEZE kan de aanroeper eigenlijk alle globale functies benaderen
// zoals __Log
hook.initialize({ S: S, Oracle: Oracle, customerId: customerId, safe: safe, DEZE: DEZE });
return hook;
}
}
api2.error(500, "Undefined plugin {0}".format(plugin_name));
},
plugin: {
transform_filter: function(filter)
{
var outdata = filter;
var hook = api2_rest.find_plugin();
if ("transform_filter" in hook)
{
outdata = hook.transform_filter(filter);
}
hook = null;
return outdata;
},
transform_incoming: function(params, data)
{
var outdata = data;
var hook = api2_rest.find_plugin();
if ("transform_incoming" in hook)
{
outdata = hook.transform_incoming(params, data);
}
hook = null;
return outdata;
},
transform_outgoing: function(params, data)
{
var outdata = data;
var hook = api2_rest.find_plugin();
if ("transform_outgoing" in hook)
outdata = hook.transform_outgoing(params, data);
hook = null;
return outdata;
}
}
}
// LET OP: Verwacht wordt dat de JSON-code in de body utf-8 encoded is, niet windows-1252!
// (in de praktijk moet je *moeite* doen om windows-1252 te krijgen dus dit is handiger)
function RequestJSON()
{
var jvraag;
if(Request.TotalBytes > 0)
{
var lngBytesCount = Request.TotalBytes;
jvraag = BytesToStr(Request.BinaryRead(lngBytesCount));
}
__Log("Vraag: " + jvraag);
try
{
var vraag = myJSON.parse(jvraag);
}
catch (e)
{
__DoLog("JSON eval faalt met: {0}