Files
Facilitor/APPL/API2/api2_rest.inc
2025-08-25 12:59:03 +00:00

1375 lines
60 KiB
PHP
Raw Blame History

<% /*
$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:
*/
%>
<!-- #include file="api2.inc" -->
<!-- #include file="api2_swagger.inc" -->
<!-- #include file="../scf/scaffolding_common.inc" -->
<%
var DEZE = this;
api2_rest = {
authenticate: function _authenticate()
{
var method = String(Request.ServerVariables("REQUEST_METHOD"));
if (user_key > 0 && method != "GET" && Session("stateless") != 1 && !Session("aut_client_key")) // Vereis dan wel het CSRF token
{
var token = Request.ServerVariables("HTTP_X_CSRF_TOKEN").Count // Meegegeven als X-CSRF-TOKEN
? String(Request.ServerVariables("HTTP_X_CSRF_TOKEN"))
: "";
protectRequest.validateToken(token);
}
if (user_key < 0)
{
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;
}
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';
var inputformat = outputformat = getQParamSafe("format", "invalid").toLowerCase();
if (outputformat == "auto")
{
var accept = String(Request.ServerVariables("HTTP_ACCEPT")).split(",")[0]; // Altijd alleen eerste bekijken
switch (accept.toLowerCase())
{
case "application/xml": outputformat = "xml"; break;
case "application/json": outputformat = "json"; break;
case "text/csv": outputformat = "csv"; break;
case "text/html": outputformat = "html"; break; // vanuit browser?
default: outputformat = "json";
}
}
if (outputformat == "json")
/* global */ JSON_Result = true; // Zelf doen we er niets mee maar
// shared.simple_page kijkt er naar
var orgError = api2.error;
api2.error = function (code, msg)
{
api2_rest.plugin.error_handler(code, msg, orgError);
}
api2_rest.authenticate();
// Kip-ei: de omzetting naar new model() mag pas als je geauthenticeerd bent
// Hierboven willen we het 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"));
method = api2_rest.plugin.transform_method(method);
if (!/GET|PUT|POST|DELETE|PATCH/.test(method)) // Overigens houdt IIS deze al eerder tegen
{
__Log("Returning status: 405 Method not allowed");
Response.Status = "405 Method not allowed";
Response.End;
}
// JGL: Met zeer grote tegenzin behoorlijk UWVA specifiek
if (Session("isWriteonlyApi2User"))
{
if (method == "PUT" || (method == "GET" && getQParam("api2") == "reports"))
{
// Dat is toegestaan
}
else
{
Response.Status = api2.error(400, "Method not supported for this user");
}
}
var key = getQParamInt("id", -1); // Voor POST/PUT/DELETE
var jsondata = {};
var filter = shared.qs2json(model);
filter = api2_rest.plugin.transform_filter(filter);
var requestparams = { filter: filter, include: getQParamArray("include", []), method: method };
if (filter.mode == "attachment" && !("includes" in model && "custom_fields" in model.includes))
api2.error(400, "Attachment not supported for this model");
if (filter.mode == "attachment")
{
if (key > 0 && method == "POST") // het bestand mag dan wel nieuw zijn (POST is van toepassing),
method = "PUT"; // intern is het een update van een bestaand record, dus PUT
}
if (!("REST_" + method in model) || !model["REST_" + method])
{
__Log("Method {0} not supported for this model, returning 501".format(method));
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;
}
if (/PUT|POST|PATCH/.test(method)) // Dan is er in de body data meegestuurd
{
if (filter.mode == "attachment" || filter.mode == "files")
{
// Body bevat application/octet-stream, dat lezen we later hier uit
var bytes = Request.TotalBytes;
if (bytes == 0)
api2.error(400, "Empty file");
var bodyStream = Server.CreateObject("ADODB.Stream");
bodyStream.Type = 1; // adTypeBinary eerst nog
bodyStream.Open();
try
{
"---";
"--- Error 'Operation not Allowed' op de volgende regel betekent meestal dat de upload te groot is";
"---";
bodyStream.Write(Request.BinaryRead(bytes));
}
catch(e)
{
var msg = L("lcl_shared_upload_error_start") + e.description + L("lcl_shared_upload_error_end");
api2.error(400, msg);
}
}
else
{
if (inputformat == "auto")
{
var contenttype = String(Request.ServerVariables("HTTP_CONTENT_TYPE")).split(";")[0]; // "text/xml; charset=utf-8", alleen de mimetype willen we
switch (contenttype.toLowerCase())
{
case "application/xml":
inputformat = "xml";
break;
case "application/json":
case "application/scim+json":
inputformat = "json";
break;
case "application/x-www-form-urlencoded":
inputformat = "form";
break;
default:
inputformat = outputformat;
}
}
switch (inputformat)
{
case "json":
{
var parsed = RequestJSON();
if (parsed.error)
api2.error(400, "Error parsing input JSON: " + parsed.error);
jsondata = api2_rest.plugin.transform_incoming(requestparams, parsed.json);
if (!jsondata)
api2.error(400, "Error parsing input JSON: Empty");
break;
}
case "xml":
{
var parsed = RequestXML();
if (parsed.error)
api2.error(400, "Error parsing input XML: " + parsed.error);
jsondata = api2_rest.xml2json(parsed.xml);
if (!jsondata)
api2.error(400, "Error parsing input XML: Empty");
break;
}
//case "form":
//{
// jsondata = { };
// jsondata[model.recordname] = shared.form2json(model); // of api2.form2JSONdata inzetten?
//}
default:
UNKNOWN_CONTENT_TYPE;
}
if (!jsondata || !(model.record_name in jsondata || (model.multi_update && model.records_name in jsondata)))
{
api2.error(400, "No '{0}' found in input".format(model.record_name));
}
else
{
// Validate the data and data types according to the model
// Throws api2.error if data is not found in model or given data is incompatible with its datatype
api2.validateJSON(jsondata, model);
}
}
}
var isSingle = /PUT|POST|DELETE|PATCH/.test(method) || (key > 0); // PUT, POST en DELETE altijd single
if (outputformat == "doc")
{
// Dan hoeven we verder bijna niets te doen
if ("autfunction" in model)
{
if (typeof model["autfunction"] == "string")
model.autfunctionname = L("lcl_" + model.autfunction);
else
delete model["autfunction"];
}
// Database hints toevoegen
var sql = "SELECT fac_locale_xsl_label, COALESCE (fac_locale_xsl_cust, fac_locale_xsl_tekst) tekst"
+ " FROM fac_locale_xsl"
+ " WHERE fac_locale_xsl_lang = " + safe.quoted_sql(user_lang)
+ " AND fac_locale_xsl_module = 'ASP'"
+ " AND fac_locale_xsl_label LIKE 'hint.{0}%'".format(model.table)
+ " AND COALESCE (fac_locale_xsl_cust, fac_locale_xsl_tekst) IS NOT NULL";
var oRs = Oracle.Execute(sql);
while (!oRs.EOF)
{
var label = oRs("fac_locale_xsl_label").Value.split(".").pop(); // Na de laatste punt
if (label in model.fields)
model.fields[label].hint = oRs("tekst").Value;
oRs.MoveNext();
}
oRs.Close()
// TODO: velden strippen waar je niets mee te maken hebt?
}
else if (outputformat == "api" && getQParamInt("swagger", 0) == 0)
{
// TODO: Onderstaande in een of ander standaardformaat opleveren?
var result = { id: model.records_name,
"name": model.record_title,
"names": model.records_title,
"records_name": model.records_name,
"record_name": model.record_name,
"authorization": model.autfunction,
methods: [],
includes: [],
fields: []
};
for (var i in model.includes)
result.includes.push(i);
if (model["REST_GET"])
result.methods.push("GET");
if (model["REST_PUT"])
result.methods.push("PUT");
if (model["REST_POST"])
result.methods.push("POST");
if (model["REST_DELETE"])
result.methods.push("DELETE");
if (model["REST_PATCH"])
result.methods.push("PATCH");
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,
required: model.fields[fld].required,
valuelist: model.fields[fld].LOV && api2.splitLOV(model.fields[fld].LOV),
translate: model.fields[fld].translate
});
}
}
else if (outputformat == "api" && getQParamInt("swagger", 0) == 1)
{
result = swaggermodel(model);
}
else
{
if (method == "DELETE")
{
if (!(key > 0))
api2.error(400, "Missing id");
var result = model["REST_" + method]( requestparams, key );
}
else if (filter.mode == "attachment")
{ // GET/PUT/POST https://xxxx.facilitor.nl/api2/visitors/99685/attachments/1040/testfile.jpg" bestaande folder
// POST https://xxxx.facilitor.nl/api2/issues/0/attachments/1040/testfile.jpg" nieuw object, maakt TEMP aan
// bodyStream heeft al een waarde gekregen
if (wasCodePage != 65001 && filter.filename)
{
// 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 jsondata = { "custom_fields": [
{
"propertyid": parseInt(filter.subfolder, 10),
"value": filter.filename, // Gaat de database in bij "F"
"attachments": [
{
"name": filter.filename // Zo gaat hij heten op het filesysteem
}
]
}
]
};
if (method == "PUT" || method == "POST")
{
jsondata["custom_fields"][0]["attachments"][0].datastream = bodyStream;
if (key == 0) // new record
{
var result = model.includes["custom_fields"].model.REST_POST(requestparams, jsondata.custom_fields[0], -1);
var token = result.token;
// Bestandsnaam kan aangepast zijn.
jsondata.custom_fields[0].value = result.jsondata.value;
jsondata.custom_fields[0].attachments[0] = result.jsondata.attachments[0];
var record = { xflexparentkey: -1, propertyid: jsondata.custom_fields[0].propertyid};
var fileparams = { getFile: jsondata["custom_fields"][0].value, api2name: null, tmpfolder: token };
}
else
{
requestparams.custom_fields_only = true; // Voorkom bij nieuwe bijlage onnodige update vam hele issue
model["REST_" + method](requestparams, jsondata, key); // via het hoofdmodel met authorisatie controle en alles
var record = { xflexparentkey: key, propertyid: jsondata.custom_fields[0].propertyid};
var fileparams = { getFile: jsondata["custom_fields"][0].value, api2name: model.records_name };
}
var data = model.includes["custom_fields"].model.get_file_info(requestparams, record, fileparams );
api2_rest.deliver(data, /* dummy model */ { record_name: "attachment" }, outputformat, true);
return jsondata; // Attachment data teruggeven. Bestandsnaam kan aangepast zijn.
}
// Geen atachment dus door met de reguliere code
var data = model["REST_" + method](requestparams, jsondata, key);
if (method == "GET")
{
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
result = data;
}
else if (method == "GET")
{
if (outputformat == "csv" || outputformat == "xlsx")
requestparams.filter.nolimit = 1;
var result = model["REST_" + method]( requestparams, null, key );
}
else if (filter.mode == "files")
{
var result = model["REST_" + method]( requestparams, bodyStream, key );
}
else if (model.record_name in jsondata) // een enkel record
{
if (method == "PUT" && !(key > 0))
api2.error(400, "Missing id");
if (jsondata[model.record_name] instanceof Array)
api2.error(400, "{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 <20><>n record
var result = {};
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":
case "PATCH":
{
if (filter.mode == "files")
{
var data = model.REST_GET({ filter: { filename: result.filename, iecode: filter.iecode }, dataonly: true }); // resulterende data weer terug
isSingle = true;
}
else
{
var key = result.key;
if (key > 0 && !Session("isWriteonlyApi2User"))
{
// 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, outputformat, isSingle);
},
// Data is een array met 'records'
deliver: function _deliver(data, model, format, single )
{
if (single && !data.length)
{
__Log("Returning status: 404 Not Found");
Response.Status = "404 Not Found";
Response.End;
}
if (format == "html" || format == "json" || format == "table" || format == "csv" || format == "xlsx")
{
var result = { };
if (model.formatted_get)
result = data;
else if (single)
result[model.record_name] = data[0];
else
{
result.total_count = model.total_count;
if (!(getQParam("justcount", 0) == 1))
{
result.limit = model.limit;
result.offset = model.offset;
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)
{
if (fld.substring(0,1) == "_")
{
delete model.fields[fld];
continue;
}
for (var prop in model.fields[fld])
{
if (typeof model.fields[fld][prop] == "function") // Bijvoorbeeld model_issues.filter
model.fields[fld][prop] = "<<function>>";
}
if (model.fields[fld].LOV)
model.fields[fld].valuelist = api2.splitLOV(model.fields[fld].LOV, "lov_");
var lclname = "{0}.{1}.hint".format(model.records_name, fld);
lcl2fld[lclname] = fld;
safefieldnames.push(safe.quoted_sql(lclname));
}
if (safefieldnames.length)
{
//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();
}
oRs.Close();
}
}
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 = true; // XSL kan includes hebben
style.validateOnParse = true; // en moet correct zijn
if (Request.QueryString("debug").Count == 0 || !user.has("WEB_FACTAB"))
{
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
{
// Het kan zijn dat de stylesheet bepaalde informatie verbergt.
// Daarom niet zo maar aanbieden
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 = "<pre>" + Server.HTMLEncode(xml_antwoord.transformNode(style)) + "</pre>";
}
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);
Response.ContentType = "application/json";
break;
case "html":
Response.ContentType = "text/html";
var antwoord = JSON.stringify(resultdata, null, 2);
var str_antwoord = "<!DOCTYPE html>\n<html>\n<head>"
+ "\n<meta http-equiv='X-UA-Compatible' content='IE=edge,chrome=1'>"
+ "\n<meta name='viewport' content='width=device-width, initial-scale=1.0, user-scalable=yes'>"
+ "\n<title>{0}</title>".format(safe.html(single?model.record_name:model.records_name))
+ "\n</head>"
+ "\n<body><pre>\n"
+ Server.HTMLEncode(antwoord)
+ "\n</pre>\n</body>\n</html>";
break;
case "csv":
Response.ContentType = "text/csv";
var str_antwoord = api2_rest.json2csv(single?[resultdata[model.record_name]]:resultdata[model.records_name], model, single);
break;
case "xlsx":
Response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
var str_antwoord = api2_rest.json2xlsx(single?[resultdata[model.record_name]]:resultdata[model.records_name], model, single);
break;
case "table":
Response.ContentType = "text/html";
if (single)
var antwoord = api2_rest.json2htmltable([resultdata[model.record_name]], model, single);
else
var antwoord = api2_rest.json2htmltable(resultdata[model.records_name], model, single);
if (getQParamInt("formail", 0) == 1)
{
var fso = new ActiveXObject("Scripting.FileSystemObject");
var fcss = fso.OpenTextFile(Server.MapPath("appl/api2/table.css"));
var tekst = fcss.ReadAll();
fcss.Close();
var styletxt = "\n<style>/*embedding api2/table.css*/\n" + tekst + "\n</style>"
}
else
{
var styletxt = "\n <link rel=stylesheet type='text/css' href='" + rooturl + "/appl/api2/table.css'>"
}
var str_antwoord = "\n<!DOCTYPE html>"
+ "\n<html>"
+ "\n<head>"
+ "\n <title>{0}</title>".format(single?model.record_name:model.records_name)
+ styletxt
+ "\n</head>"
+ "\n<body>"
+ antwoord
+ "\n</body>"
+ "\n</html>";
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 eTag = api2_rest.plugin.get_eTag({}, resultdata);
if (!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);
},
// Merk op dat (onbedoeld) de tabel-kolommen bepaald worden door de velden in het *eerste* record
// en niet zozeer door het gedefinieerde fields-object
// Ook: verwacht wordt dat de volgorde van velden altijd gelijk is wat JavaScript 'for' niet
// garandeert (maar in de praktijk wel goed gaat)
json2htmltable: function _json2htmltable(data, model, single)
{
var ths = [];
if (!data.length)
return "\n<table>\n</table>"
// Header maken
var ths = [];
for (var fld in data[0])
{
var lbl = fld;
if (fld in model.fields)
lbl = model.fields[fld].label;
else
{
var val = data[0][fld];
if (val && typeof val == "object")
{
if (model.includes && fld in model.includes)
var lbl = model.includes[fld].model.records_title;
else
lbl = fld; // Attachments/flexfiles
}
}
ths.push("<th title='{0}'>{1}</th>".format(safe.htmlattr(lbl), fld));
}
var trs = [];
for (var i = 0; i < data.length; i++)
{
var rec = data[i];
var tds = [];
for (var fld in data[0]) // Hier dezelfde kolomvolgorde (van eerste record) gebruiken
{
var cls = "";
if (model.fields[fld] && model.fields[fld].typ.match(/number|float|currency|key/) && !model.fields[fld].foreign)
{
cls = " style='text-align:right'";
}
if (fld in rec)
var val = rec[fld];
else
var val = "";
var safeval = Server.HTMLEncode(String(val));
if (val === null)
safeval = '&nbsp;';
else if (typeof val == 'object' && val instanceof Date)
safeval = toISODateTimeString(val);
else if (val && typeof val == "object" && "id" in val)
{
var naam = val.name||"???";
if (typeof naam == 'object' && naam instanceof Date) // Bij appointment
naam = toISODateTimeString(naam);
safeval = val.id + " <em>(" + safe.html(naam) + ")</em>";
}
else if (val && typeof val == "object" && model.includes && fld in model.includes)
safeval = api2_rest.json2htmltable(val, model.includes[fld].model, true); // dereference
else if (val instanceof Array) // attachments array
safeval = api2_rest.json2htmltable(val, model, single);
if (!single && model.primary && model.fields[fld] && model.fields[fld].dbs == model.primary)
safeval = "<a href='{0}/{1}.table'>{1}</a>".format(model.records_name, safeval);
if (fld == "content_url")
safeval = "<a href='{0}' target='_blank'>{1}</a>".format(safe.htmlattr(val), safeval);
tds.push("<td{0}>{1}</td>".format(single?"":cls, safeval));
}
trs.push(tds.join(""));
}
if (single && data.length == 1)
{
var html = "";
for (var i = 0; i < ths.length; i++)
html += "\n<tr>" + ths[i] + tds[i] + "</tr>";
return "\n<table>\n" + html + "\n</table>";
}
else
return "\n<table style='border-collapse: separate;'>"
+ "\n<thead><tr style='position:sticky;top:0'>" + ths.join("") + "</tr>\n</thead>"
+ "\n<tbody>\n<tr>" + trs.join("</tr>\n<tr>") + "</tr>\n</tbody>\n</table>";
},
json2csv: function _json2csv(data, model, single)
{
var trs = [];
if (getQParam("nocsvheader",0)==0)
{ // Header maken
var tds = [];
for (var fld in data[0])
{
tds.push(fld);
}
trs.push(tds.join(";"));
}
// Nu de rows
for (var i = 0; i < data.length; i++)
{
var rec = data[i];
var tds = [];
for (var fld in rec)
{
var field = model.fields[fld];
var val = rec[fld];
if (val == null)
safeval = "";
else
{
var safeval = String(val);
if (field.typ == "varchar" || field.typ == "memo")
safeval = safeval.replace(/\x0D\x0A/g,'\x0A').replace(/\"/g,'""'); // Voorzichtig met newlines
if (field.typ == "date" || field.typ == "datetime")
safeval = toISODateTimeString(val);
else if (val && typeof val == "object" && "id" in val)
{
var naam = val.name||"???";
if (typeof naam == 'object' && naam instanceof Date) // Bij appointment
naam = toISODateTimeString(naam);
safeval = val.id + " (" + Server.HTMLEncode(naam) + ")";
}
else if (val && typeof val == "object" && model.includes && fld in model.includes)
api2.error(500, "Include={0} not supported with csv".format(fld));
else if (val instanceof Array) // attachments array
api2.error(500, "Include={0} not supported with csv".format("attachments"));
if (field.typ != "number" && field.typ != "check" && field.typ != "check0")
safeval = "\"" + safeval + "\"";
}
tds.push(safeval);
}
trs.push(tds.join(";"));
}
return trs.join("\r\n");
},
// Zie ook resultset_table_v2.inc/ResultsetTable.prototype.ProcessAsNativeExcel
json2xlsx: function _json2xlsx(data, model, single)
{
var oExcel = new ActiveXObject("SLNKXLSX.Excel");
var tmpxlsx = "XLSX_" + shared.random(16) +".xlsx"
var oWb = oExcel.workbook_new(shared.tempFolder() + "/" + tmpxlsx, 1); // 1=constant memory
oWb.set_property("author", user.naam());
oWb.set_property("title", model.records_name);
oWb.set_property("comments", "Created with Facilitor v{0}".format(FCLTVersion));
var oWs = oWb.add_worksheet("Sheet1");
var maxlengths = [];
// Header row
oWs.freeze_panes(1, 0);
var col = 0;
for (var fld in data[0])
{
var caption = fld; // Bij API2 beter dan model.field[fld].label omdat het iets meer Machine2Machine is.
if (fld in model.fields) // niet bij includes
{
var tooltip = model.fields[fld].label;
if (tooltip)
oWs.write_comment(0, col, tooltip); // maar wel label in het commentaar
}
var len = oWs.write_string(0, col++, caption, 1); // 1 = bold
maxlengths.push(len);
oWs.set_column_width(i, 8.43 /* LXW_DEF_COL_WIDTH */, 1); // Forceer alvast 'wrap' omdat met constant memory het niet achteraf kan
}
// Nu de rows
for (var row = 0; row < data.length; row++)
{
var i = 0;
var rec = data[row];
var rownum = row + 1;
for (var fld in rec)
{
var field = model.fields[fld];
var val = rec[fld];
if (val == null)
{
i ++;
continue;
}
if (val && typeof val == "object" && "id" in val)
{
var naam = val.name||"???";
//if (typeof naam == 'object' && naam instanceof Date) // Bij appointment
// naam = toISODateTimeString(naam);
val = naam; // TODO: val.id in tooltip?;
field.typ = "varchar";
}
else if (val && typeof val == "object" && model.includes && fld in model.includes)
api2.error(500, "Include={0} not supported with xlsx".format(fld));
else if (val instanceof Array) // attachments array
api2.error(500, "Include={0} not supported with xlsx".format("attachments"));
if (typeof val == "object" && isNaN(val))
{
api2.error(500, "Field '{0}' value is NaN in record {1}".format(fld, row));
}
//if (typeof val == "string")
// field.typ = "varchar";
var len = 0;
//if (typeof val == "string" && inArray(field.typ, ["date", "datetime", "time", "currency", "float", "number"]))
//{
// if (!(this.columns[i].orgContent instanceof Function))
// __DoLog("Vreemd: kolom {0} is string en niet van het type {1}?".format(this.columns[i].caption, this.columns[i].datatype), "#ff0000");
// len = oWs.write_string(rownum, i, val);
//}
//else
switch (field.typ)
{
case "varchar":
case "memo" : len = oWs.write_string(rownum, i, String(val));
break;
case "date" : oWs.write_vardate(rownum, i, new Date(val).getVarDate(), 0); //TODO .midnight() toepassen eindigt in verkeerde tijdzone?
len = 16;
break;
case "datetime": oWs.write_vardate(rownum, i, new Date(val).getVarDate(), 1);
len = 10;
break;
case "time" : // In de database ondersteunen we dit niet maar komt voor bij fac_report
oWs.write_vardate(rownum, i, new Date(val).getVarDate(), 2);
len = 5;
break;
case "currency": oWs.write_number(rownum, i, val, 1);
break;
case "float" : oWs.write_number(rownum, i, val, 1);
break;
case "key":
case "check0":
case "check":
case "number": oWs.write_number(rownum, i, val, 0);
break;
case "processingtime":
if (val["duration"] != null)
val = val["duration"] + " " + (val["unit"]=='U'?L("lcl_mld_hours"):L("lcl_mld_days"));
else
val = "";
len = oWs.write_string(rownum, i, val);
break;
case "html": val = val.replace(/(<([^>]+)>)/ig,""); // strip html
val = val.replace(/\&nbsp\;/ig, " ");// &nbsp; naar spatie;
val = val.replace(/\&amp;\;/ig, "&");// er zijn er nog meer maar dit vind ik genoeg
val = val.replace(/\&lt\;/ig, "<");
val = val.replace(/\&gt\;/ig, ">");
val = val.replace(/\&apos\;/ig, "'>'");
val = val.replace(/\&quot\;/ig, "\"");
// TODO: andere html-entities ook vertalen?
len = oWs.write_string(rownum, i, val);
break;
default: len = oWs.write_string(rownum, i, String(val));
}
if (len > maxlengths[i])
maxlengths[i] = len;
i++;
}
}
for (var i = 0; i < maxlengths.length; i++)
{
if (maxlengths[i] > 8.43 * 10) // LXW_DEF_COL_WIDTH, 8.43 is de default Excel cell width
{
oWs.set_column_width(i, Math.min(maxlengths[i] / 10, 50), 1);
}
}
oWb.workbook_close();
var mime = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
var filename = "Facilitor {0} {1}.xlsx".format(model.records_name, toISODateTimeString(new Date()));
var pcontentdp = "attachment";
Response.AddHeader("content-disposition", pcontentdp + "; filename= \"" + safe.ContentName(filename) + "\"")
StreamFile(shared.tempFolder(), tmpxlsx, { mime: mime ,
pcontentdp: "none", // geen pcontentdp, dat hebben we zelf gedaan
deletefile: __Logging == 0});
},
// 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);
arrayElement.setAttribute("total_count", model.total_count);
arrayElement.setAttribute("limit", model.limit);
arrayElement.setAttribute("offset", model.offset);
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.hasChildNodes())
return null; // beter dan een leeg object
}
}
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(inErrorHandler)
{
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)
{
var txt = "Loading {0} failed: {1}".format(ppath, e.description);
if (inErrorHandler)
{
Response.Write(txt); // Zeer fataal. Probeer niet meer fancy te doen.
Response.End;
}
else
api2.error(500, txt);
}
// 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;
}
}
abort_with_warning("Undefined plugin {0}".format(plugin_name), 500);
//api2.error(500, "Undefined plugin {0}".format(plugin_name)); // Geen API2 aanroep als je al in de problemen zit: oneindige recursie.
},
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_method: function(method)
{
var outmethod = method;
var hook = api2_rest.find_plugin();
if ("transform_method" in hook)
{
outmethod = hook.transform_method(method);
}
hook = null;
return outmethod;
},
error_handler: function(code, msg, orgHandler)
{
var hook = api2_rest.find_plugin(true);
if ("error_handler" in hook)
outdata = hook.error_handler(code, msg, orgHandler);
else
outdata = orgHandler(code, msg);
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;
},
get_eTag: function(params, data)
{
var outdata = null;
var hook = api2_rest.find_plugin();
if ("get_eTag" in hook)
outdata = hook.get_eTag(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;
"---";
"--- Error 'Operation not Allowed' op de volgende regel betekent meestal dat de upload te groot is";
"---";
jvraag = BytesToStr(Request.BinaryRead(lngBytesCount));
}
__Log("Request body: " + jvraag);
try
{
var vraag = myJSON.parse(jvraag);
}
catch (e)
{
result = { error: e.description || (e.message + ": " + e.name) };
__DoLog("JSON parse faalt met: '{0}'\n{1}".format(result.error, jvraag), "ffff00");
return result ;
}
return { json: vraag };
}
function RequestXML()
{
var xvraag;
if(Request.TotalBytes > 0)
{
var lngBytesCount = Request.TotalBytes;
"---";
"--- Error 'Operation not Allowed' op de volgende regel betekent meestal dat de upload te groot is";
"---";
xvraag = BytesToStr(Request.BinaryRead(lngBytesCount));
}
__Log("Request body: " + xvraag);
try
{
var inputXML = Server.CreateObject("MSXML2.DOMDocument.6.0");
inputXML.loadXML(xvraag); // inputXML.load(Request) zou nog niet vertalen naar Windows-1252
}
catch (e)
{
return { error: e.description };
}
if (inputXML.parseError.errorCode)
{
return { error: inputXML.parseError.reason + " @ " + inputXML.parseError.line + "." + inputXML.parseError.linepos };
}
return { xml: inputXML };
}
function BytesToStr(bytes)
{
var stream = Server.CreateObject("ADODB.Stream");
stream.type = 1;
stream.open;
stream.write(bytes);
stream.position = 0;
stream.type = 2; // Text
stream.charset = "utf-8";
// Merk op dat we windows-1252 niet ondersteunen, je moet echt utf-8 sturen
// Mogelijk was het nog utf-8. Dat is riskant met unicode quotes (FCLT#60591)
// Daarom expliciete conversie
var win1252Stream = new ActiveXObject("ADODB.Stream");
win1252Stream.Open();
win1252Stream.CharSet = "Windows-1252"; // het doel
stream.copyTo(win1252Stream);
stream.close;
win1252Stream.position = 0;
var ReadBytes = 131072; // optimaal volgens https://docs.microsoft.com/en-us/sql/ado/reference/ado-api/readtext-method?view=sql-server-ver15
var sOut = "";
while (!win1252Stream.EOS)
{
sOut = sOut + win1252Stream.ReadText(ReadBytes);
}
win1252Stream.close;
return sOut;
}
function getQParamISODate(pName, defVal)
{
return _get_ParamISODate(Request.Querystring, pName, defVal)
}
function getFParamISODate(pName, defVal)
{
return _get_ParamISODate(Request.Form, pName, defVal)
}
function _get_ParamISODate(pColl, pName, defVal)
{
var strval = _get_Param(pColl, pName, defVal, true); // force: een lege waarde wordt als afwezig beschouwd
if (strval)
{
var dt = myJSON.internal_parsedate(null, strval);
if (dt && dt instanceof Date)
return dt;
}
if (defVal instanceof Date)
{
return defVal;
}
if (defVal === null) // bewust triple===
{
return null;
}
// Error message will get to client and/or IIS logfiles
_AiAi("INTERNAL_ERROR_PARAMETER_" + pName + "_IS_NOT_ISODATE");
}
var simple_json2xml = (function (undefined) {
"use strict";
var tag = function (name, closing) {
return (closing ? "</" : "\n<") + name + ">";
};
return function (obj, rootname) {
var xml = "";
for (var i in obj) {
if (obj.hasOwnProperty(i)) {
var value = obj[i],
type = typeof value;
if (value === obj) // bij merged_model zouden we hier oneindige recursie krijgen
continue;
if (value instanceof Array && type == 'object') {
for (var sub in value) {
xml += simple_json2xml(value[sub]);
}
} else if (value instanceof Object && type == 'object') {
xml += tag(i) + simple_json2xml(value) + tag(i, 1);
} else {
xml += tag(i) + (value===null?"":Server.HTMLEncode(value)) + tag(i, 1);
}
}
}
return rootname ? tag(rootname) + xml + tag(rootname, 1) : xml;
};
})(simple_json2xml || {});
%>