1 2 /****************************************************************************************************************************** 3 * (c) ITSV GmbH - http://www.itsv.at/ 4 * 5 * Software Operations 6 * Zentrale Daten und Services 7 * Wolfgang Scherer 8 * 9 * CCDB - Competence Center Controlling Database 10 * 11 * MODULE: ccdb.js 12 * 13 * SEE ALSO: http://zpvwiki.sozvers.at/wiki/ZPV_Controlling_Datenbank#App 14 * 15 * HISTORY: 16 * VERSION DATE CHANGES 17 * 0.1 08.08.2015 created with RANGE 18 * 0.2 15.08.2015 multi-database with selection page 19 * /show_cpu graphically with D3-Library 20 * generic /query and /sequence 21 * 0.3 09.09.2015 replace /show_cpu with query configuration 22 * load_xls with XLSX library 23 * 0.4 06.10.2015 session management, LOGIN/password management, authorization rights check 24 * 0.5 28.10.2015 remove /sequence, replaced by querytype='sequence' 25 * query result format 'hhlist' 26 * 0.6 04.11.2015 DBrequest new attribute 'reqid', identifies the request in order to be able to substitute 27 * other SQL statements for other databases (current SQL assumes oracle RDBMS with oracledb nodejs driver) 28 * relocate auxiliary functions to aux/auxiliary.js 29 * relocate modular business functions to functions/functions.js 30 * 0.7 11.11.2015 re-factor load_csv and load_xls to a common load_file 31 * 0.71 16.11.2015 allow options in SEQUENCE steps, sparated from QUERY-Name of step by ':' 32 * clean-up DSEQUENCE by processing all steps in the same state and by the same code 33 * 0.8.099 20.11.2015 make /dquery a POST action 34 * start changing to query parameter processing 0.8: 35 * each query has a 'paramtype', no more 'paramnames' or 'attributenames' 36 * the paramtype can have multiple attributes, there go the query parameters !! 37 * param form processing will benefit from this 38 * 39 ******************************************************************************************************************************/ 40 41 42 43 44 var script_version = "0.8.102"; 45 var st = new Date(); // force Date.prototype to be created 46 47 // format date as YYYYMMDDhhmmss.ttt to create a unique sortable time string up to 1 msec in precision 48 function dateTimStr(theDate) { 49 var yyyy = theDate.getFullYear().toString(); 50 var mm = "00"+(theDate.getMonth()+1).toString(); // getMonth() is zero-based 51 var dd = "00"+theDate.getDate().toString(); 52 var hh = "00"+theDate.getHours().toString(); 53 var min = "00"+theDate.getMinutes().toString(); 54 var sec = "00"+theDate.getSeconds().toString(); 55 var mil = "000"+theDate.getMilliseconds().toString(); 56 return yyyy + "-" + mm.substring(mm.length-2) + "-" + dd.substring(dd.length-2) + "_" + 57 hh.substring(hh.length-2) + ":" + min.substring(min.length-2) + ":" + sec.substring(sec.length-2) + "." + mil.substring(mil.length-3); // padding 58 }; 59 // enhance Date by .timstr() 60 Date.prototype.timstr = function() { 61 var yyyy = this.getFullYear().toString(); 62 var mm = "00"+(this.getMonth()+1).toString(); // getMonth() is zero-based 63 var dd = "00"+this.getDate().toString(); 64 var hh = "00"+this.getHours().toString(); 65 var min = "00"+this.getMinutes().toString(); 66 var sec = "00"+this.getSeconds().toString(); 67 var mil = "000"+this.getMilliseconds().toString(); 68 return yyyy + mm.substring(mm.length-2) + dd.substring(dd.length-2) + 69 hh.substring(hh.length-2) + min.substring(min.length-2) + sec.substring(sec.length-2) +mil.substring(mil.length-3); // padding 70 }; 71 // node.js internal modules 72 73 var https = require('https'); 74 var http = require('http'); 75 var util = require('util'); 76 var crypto = require('crypto'); 77 var path = require('path'); 78 var os = require('os'); 79 80 // additional modules installed via NPM 81 82 var querystring = require('querystring'); 83 var express = require('express'); 84 var express = require('express'); 85 var session = require('express-session'); 86 // var oracledb = require("oracledb"); 87 var mysqldb = require("mysql"); 88 var favicon = require('serve-favicon'); 89 var multer = require('multer'); 90 var bodyParser = require('body-parser'); 91 var formidable = require('formidable'); 92 var flow = require('flow'); 93 var fs = require('fs'); 94 var XLSX = require('xlsx'); 95 var winston = require('winston'); 96 97 // setup central logger instance using winston 98 99 // require('winston-riak').Riak; 100 // require('winston-mongo').Mongo; 101 var logger = new (winston.Logger)({ 102 transports: [ 103 new (winston.transports.Console)({ // console logger with custom formatter (according to https://github.com/winstonjs/winston#adding-custom-transports) 104 timestamp: function() { 105 return dateTimStr(new Date()); 106 }, 107 formatter: function(options) { 108 // Return string will be passed to logger. 109 return ' ' + options.timestamp() +' '+ options.level.toUpperCase() +' '+ (undefined !== options.message ? options.message : '') + 110 (options.meta && Object.keys(options.meta).length ? '\n\t'+ JSON.stringify(options.meta,null,2) : '' ); 111 } 112 }), 113 // new winston.transports.Console(), 114 new winston.transports.File({ filename: __dirname+'/logs/all-logs.log' }) 115 // new winston.transports.Couchdb({ 'host': 'localhost', 'db': 'logs' }), 116 // new winston.transports.Riak({ bucket: 'logs' }), 117 // new winston.transports.MongoDB({ db: 'db', level: 'info'}), 118 ], 119 exceptionHandlers: [ 120 new (winston.transports.Console)({ 121 timestamp: function() { 122 return dateTimStr(new Date()); 123 }, 124 formatter: function(options) { 125 // Return string will be passed to logger. 126 var rs = ' ' + options.timestamp() +' EXCEPTION '+ options.level.toUpperCase() +' '+ (undefined !== options.message ? options.message : ''); 127 if (options.meta && Object.keys(options.meta).length) { 128 rs += '\n'+ JSON.stringify(options.meta,null,2); 129 } 130 return rs; 131 } 132 }), 133 new winston.transports.File({ filename: __dirname+'/logs/exceptions.log' }) 134 ] 135 }); 136 137 logger = expandErrors(logger); 138 139 // Extend a winston by making it expand errors when passed in as the 140 // second argument (the first argument is the log level). 141 function expandErrors(logger) { 142 var oldLogFunc = logger.log; 143 logger.log = function() { 144 var args = Array.prototype.slice.call(arguments, 0); 145 if (args.length >= 2 && args[1] instanceof Error) { 146 args[1] = args[1].stack; 147 } 148 return oldLogFunc.apply(this, args); 149 }; 150 return logger; 151 } 152 153 logger.info("Logging with winston enabled"); 154 logger.debug("DEBUG Logging NOT enabled"); 155 logger.level = 'debug'; 156 logger.debug("DEBUG Logging enabled"); 157 158 require('../../JSON-js/cycle.js'); 159 160 var global_stored_results = {info: "CCDB global stored results"}; 161 var global_active_actions = {info: "CCDB global active actions"}; 162 163 var prefs = {}; // global configurations preferences 164 prefs.restart_timeout_ms = 60000; // restart timeout is 1 minute 165 prefs.restart_exit_code = 92; // exit code to request restart 166 prefs.shutdown_exit_code = 91; // exit code to request permanent shutdown 167 prefs.halt_exit_code = 99; // exit code to request termination 168 prefs.do_not_catch_in = {}; 169 prefs.do_not_catch_in['qexpression'] = true; // control catching exceptions in query jsexpr expressions 170 prefs.do_not_catch_in['aexpression'] = true; // control catching exceptions in query jsexpr expressions 171 172 prefs.global_stored_results = global_stored_results; 173 prefs.global_active_actions = global_active_actions; 174 175 prefs.appbase = __dirname; 176 prefs.uploaddir = prefs.appbase + "/uploads"; 177 178 // CCDB modules 179 180 var aux = require('./auxiliary.js')(logger,"ccdb",prefs); 181 var db = require('./db.js')(logger,"ccdb",prefs); 182 var dbConfig = require('./dbConfig.js'); 183 var dbconfig = db.getCurrentConfig(); 184 var credentials = require('./credentials.js'); 185 var functions = require('./functions.js')(logger,"ccdb",prefs); 186 187 var storage = multer.diskStorage({ 188 destination: function (req, file, cb) { 189 cb(null, prefs.uploaddir) 190 }, 191 filename: function (req, file, cb) { 192 cb(null, file.fieldname + '-' + Date.now()) 193 } 194 }) 195 196 var upload = multer({ storage: storage, dest: prefs.uploaddir }) 197 198 var app = express(); 199 app.use(favicon(__dirname + '/html/favicon.ico')); // to insert FAVICON in responses 200 app.use(bodyParser.json()); // for parsing application/json 201 app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded 202 app.use(session({resave: false, 203 saveUninitialized: false, 204 secret: credentials.cookieSecret 205 })); 206 app.use(function(req,res,next) { // try middleware, TODO: does not work yet, find out why !! 207 // logger.debug("CCDB_MW.BEGIN.REQ:"); 208 // logger.debug(req); 209 if (!(req.session)) { // if we don't have a session ... 210 logger.error("REQ has no session"); 211 res.redirect(303,'/login'); // ... redirect the user to the login page 212 } else { 213 next(); 214 } 215 }); 216 217 218 219 /* 220 * CLASS: ManagedFile 221 */ 222 function ManagedFile(id,path) { 223 this.mfid = id; 224 this.mfpath = path; 225 this.getId = function() { 226 return this.mfid; 227 } 228 this.getPath = function() { 229 return this.mfpath; 230 } 231 } 232 233 234 // 235 // load and activate handlebars templating engine 236 // 237 var handlebars = require('express-handlebars') 238 .create({ defaultLayout: 'main', 239 helpers: { 240 // the section helper, allows insertion of text into sections out of {{body}} 241 section: function(name, options){ 242 if(!this._sections) this._sections = {}; 243 this._sections[name] = options.fn(this); 244 return null; 245 }, 246 // the 'group-by' helper, allows grouping in hierarchical lists 247 group: function (list, options) { 248 options = options || {}; 249 var fn = options.fn || aux.noop, 250 inverse = options.inverse || aux.noop, 251 hash = options.hash, 252 prop = hash && hash.by, 253 keys = [], 254 groups = {}; 255 if (!prop || !list || !list.length) { 256 return inverse(this); 257 } 258 function groupKey(item) { 259 var key = aux.get(item, prop); 260 if (keys.indexOf(key) === -1) { keys.push(key); } 261 if (!groups[key]) { groups[key] = {value: key,items: []};} 262 groups[key].items.push(item); 263 } 264 function renderGroup(buffer, key) { 265 return buffer + fn(groups[key]); 266 } 267 list.forEach(groupKey); 268 return keys.reduce(renderGroup, ''); 269 } 270 } 271 } 272 ); 273 274 app.engine('handlebars', handlebars.engine); 275 app.set('view engine', 'handlebars'); 276 277 // 278 // default port to listen on is TCP/3000 279 // 280 app.set('port', process.env.PORT || 3000); 281 282 // 283 // provide static web pages in "html" subdirectory 284 // 285 app.use(express.static('html')); 286 287 /* ***************************************************************************** 288 * 289 * ROUTE: / 290 * 291 * the "home" page 292 */ 293 app.get('/', 294 function(req,res) { 295 if (sessionIsLoggedIn(req.session)) { 296 res.render('plain',{body: 'CCDB ist ein Management-Instrument für Competence-Centers'}) 297 } else { 298 res.redirect(303,'/login'); 299 } 300 }); 301 302 /* ***************************************************************************** 303 * 304 * ROUTE: /shutdown 305 * 306 * initiate restarting the server 307 * to achieve this, the OS script invoking node.js must check the return status of this program 308 * this action will terminate the server with the return status code (exit code) taken from the 309 * "restart_exit_code" <prefs.restart_exit_code> preference, defaulting to 942 310 * to somewhat lessen the disruptive impact of the termination, a restart will occur after a delay 311 * of <prefs.restart_timeout_ms>, defaulting to 60000 (1 Minute) 312 * the invoking OS script must detect the restart return status code and start the server again 313 */ 314 app.get('/shutdown', 315 function(req,res) { 316 if (!(req.query.hasOwnProperty('mode'))) { 317 logger.error("/shutdown without mode parameter"); 318 res.redirect(303,"/error"); 319 return; 320 } 321 if (!req.session) { 322 res.render('error',{errmsg: "Shutdown "+req.query.mode+" kann nicht ausgeführt werden, keine Session verfügbar"}); 323 return; 324 } 325 if (sessionIsLoggedIn(req.session)) { 326 functions.check_authorization(req.session,'shutdown_'+req.query.mode,req, 327 function(err,result) { 328 if (err) { 329 res.render('error',{errmsg: err.message}); 330 return; 331 } 332 prefs.server.close( function(err) { 333 if (err) { 334 logger.error("CCDR.ERROR_CLOSING.MESSAGE="+err.message); 335 } 336 logger.debug("CCDB: HTTPS server closed"); 337 } 338 ); 339 var reason; 340 switch (req.query.mode) { 341 case "shutdown": 342 reason = "um abgeschaltet zu werden"; 343 break; 344 case "restart": 345 reason = "um neu gestartet zu werden"; 346 break; 347 default: 348 reason = "aus unbekanntem Grund"; 349 } 350 res.render('plain',{body: "Server wird in spätestens "+prefs.restart_timeout_ms+" ms "+ 351 " beendet, "+reason+". Bitte <a href=\"/logout\">abmelden</a>, um die Beendigung zu beschleunigen."}); 352 init_shutdown(req.query.mode); 353 }); 354 } else { 355 res.render('error',{errmsg: "nicht angemeldet"}); 356 } 357 }); 358 359 /* ***************************************************************************** 360 * 361 * ROUTE: /login 362 * 363 * DESCRIPTION: the login page 364 */ 365 app.get('/login', 366 function(req,res) { 367 if (sessionIsLoggedIn(req.session)) { 368 res.redirect(303,'/logout'); 369 } else { 370 res.render('loginform'); 371 } 372 }); 373 374 /* ***************************************************************************** 375 * 376 * ROUTE: /do_login 377 * 378 * DESCRIPTION: process the login request form 379 */ 380 app.post('/do_login',function(req,res) { 381 var form = new formidable.IncomingForm(); 382 form.keepExtensions = true; 383 form.parse(req, function(err, fields, files){ 384 if (err) { 385 var errmsg = "Error in parsing database select form: "+err.message; 386 res.render('error',{errmsg: errmsg}); 387 } 388 req.username = fields.username; 389 req.password = fields.password; 390 req.new_password = fields.new_password; 391 req.new_password_repeat = fields.new_password_repeat; 392 do_login(req,res,function(err,result) { 393 if (err) { 394 res.render('loginform',{errortext: err.message}); 395 return; 396 } 397 res.render('plain',{body: result.body}); 398 }); 399 }); 400 }); 401 402 /* ***************************************************************************** 403 * FUNCTION: sessionLoggedIn 404 * INPUT: SESS - session object 405 * RESULT: TRUE if session is logged in, FALSE otherwise 406 * DESCRIPTION: checks if session is logged in. This is done by checking 407 * if the session has a 'username' attribute 408 */ 409 function sessionIsLoggedIn(sess) { 410 return sess.username?true:false; 411 } 412 413 /* ***************************************************************************** 414 * FLOW: do_login 415 * INPUT: req - request object 416 * CALLBACK: rfunc - callback function to be called upon completion 417 * PARAMETERS: 418 * err - error object if an error occured, otherwise null 419 * result - result object: 420 * body - text with information for user logged in 421 */ 422 var do_login = flow.define( 423 // step 1: get user info from CCUS and rights list(s) from CCATT 424 function(req,res,rfunc) { 425 if (sessionIsLoggedIn(req.session)) { 426 aux.error_callback(rfunc,null,'Bereits angemeldet (<a href="/logout">abmelden</a>)'); 427 return; 428 } 429 if (prefs.init_shutdown) { 430 aux.error_callback(rfunc,null,"Server fährt herunter, Anmeldung nicht möglich, bitte später wieder <a href=\"/login\">Anmelden</a>"); 431 return; 432 } 433 this.req = req; 434 this.callback = rfunc; 435 this.req.session.username = this.req.username; 436 this.usdbr = new db.Request({info: "get user data for \""+this.req.session.username+"\"", 437 reqid: 'RQID_GETUSRDATA', 438 sql: "SELECT us.USKURZZ, us.USVORNAME, us.USFAMILIENNAME, upw.ATTVALUE as HASHED_PASSWORD, ups.ATTVALUE as PASSWORD_SALT, "+ 439 " wm_concat(DISTINCT ur.ATTVALUE) || ',' || wm_concat(DISTINCT gr.ATTVALUE) as RIGHTSLIST "+ 440 "FROM CCVUS us "+ 441 " left outer join CCATT upw on upw.ATTPARKURZ=us.USKURZZ and upw.ATTARCHTYP='USPWD' and upw.ATTNAME='HASHED_PASSWORD' and upw.ATTPARTYPE='CCUS' "+ 442 " left outer join CCATT ups on ups.ATTPARKURZ=us.USKURZZ and ups.ATTARCHTYP='USPWD' and ups.ATTNAME='PASSWORD_SALT' and ups.ATTPARTYPE='CCUS' "+ 443 " left outer join CCUSAS ua on ua.USKURZZ=us.USKURZZ "+ 444 " left outer join CCATT ur on ur.ATTARCHTYP='AUTHORIGHT' and ur.ATTPARTYPE='CCUS' and ur.ATTPARKURZ=us.USKURZZ and ur.ATTNAME='RIGHTSLIST' "+ 445 " left outer join CCATT gr on gr.ATTARCHTYP='AUTHORIGHT' and gr.ATTPARTYPE='CCGRP' and gr.ATTPARKURZ=ua.GRP and gr.ATTNAME='RIGHTSLIST' "+ 446 " where us.USKURZZ=:USERNAME "+ 447 " group by us.USKURZZ,us.USVORNAME,us.USFAMILIENNAME, upw.ATTVALUE, ups.ATTVALUE", 448 params: [this.req.session.username]}); 449 logger.debug("DO_LOGIN."+this.usdbr.info); 450 db.select(this.usdbr,this); 451 }, 452 // step 2: got user info, check password 453 function(err,result) { 454 if (err || !(result) || !(result.rows) || !(result.rows[0]) || (result.rows[0].length<6) || (result.rows[0][0]!=this.req.session.username)) { 455 var nerr = new Error("LOGIN.ERROR.Illegal Username or Password"); 456 logger.error(nerr.message); 457 logger.error(this.usdbr); 458 this.callback(nerr); 459 return; 460 } 461 this.uskurzz = result.rows[0][0] 462 this.vorname = result.rows[0][1]; 463 this.familienname = result.rows[0][2]; 464 this.hashed_password = result.rows[0][3]; 465 this.password_salt = result.rows[0][4]; 466 this.rightslist = result.rows[0][5]; 467 logger.debug("DO_LOGIN.USER_RECORD=\""+this.vorname+" "+this.familienname+"\""); 468 if (!(this.hashed_password) || this.hashed_password=='*') { 469 // no password set, grant access 470 this.have_to_change_password = true; 471 logger.debug("DO_LOGIN.NO_PASSWORD_SET"); 472 } else { 473 this.hashed_entered_password = crypto.pbkdf2Sync(this.req.password, this.password_salt, 4096, 20, 'sha256').toString('base64'); 474 logger.debug("DO_LOGIN.HASHED_PASSWORD=\""+this.hashed_password+"\".HASHED_ENTERED_PASSWORD=\""+this.hashed_entered_password+"\""); 475 if (this.hashed_password!=this.hashed_entered_password) { 476 var nerr = new Error("LOGIN.ERROR.Illegal Username or Password"); 477 logger.error(nerr.message); 478 this.callback(nerr); 479 return; 480 } 481 } 482 this.req.session.vorname = this.vorname; 483 this.req.session.familienname = this.familienname; 484 this.req.session.rightslist = this.rightslist.split(","); 485 // now we're logged in, check if user wanted to change the password 486 if (this.req.new_password) { 487 if (this.req.new_password!=this.req.new_password_repeat) { 488 var nerr = new Error("LOGIN.ERROR.new passwords are not equal"); 489 logger.error(nerr.message); 490 this.callback(nerr); 491 return; 492 } 493 // make a new salt 494 this.password_salt = crypto.randomBytes(20).toString('base64'); 495 this.new_hashed_password = crypto.pbkdf2Sync(this.req.new_password, this.password_salt, 4096, 20, 'sha256').toString('base64'); 496 this.pwdbr = new db.Request({info: "Change password for "+this.req.session.username, 497 reqid: 'RQID_CHGPWD', 498 sql: "MERGE INTO CCATT pws "+ 499 " USING (SELECT :USERNAME as ATTPARKURZ, :HASHED_PASSWORD as ATTVALUE from dual) pwu "+ 500 " ON (pws.ATTARCHTYP='USPWD' and pws.ATTPARTYPE='CCUS' and pws.ATTPARKURZ=pwu.ATTPARKURZ and pws.ATTNAME='HASHED_PASSWORD') "+ 501 " WHEN MATCHED THEN UPDATE SET pws.ATTVALUE = pwu.ATTVALUE "+ 502 " WHEN NOT MATCHED THEN INSERT (ATTARCHTYP,ATTPARTYPE,ATTPARKURZ,ATTNAME,ATTVALUE) "+ 503 " VALUES('USPWD','CCUS',pwu.ATTPARKURZ,'HASHED_PASSWORD',:HASHED_PASSWORD)", 504 params: [this.req.session.username, this.new_hashed_password]}); 505 db.select(this.pwdbr,this); 506 } else { 507 if (this.have_to_change_password) { 508 var nerr = new Error("Kennwort muss geändert werden"); 509 this.callback(nerr); 510 return; 511 } 512 login_register_session(this.req.session); 513 var result = {resulttype: 'string', body:this.req.session.vorname+" "+this.req.session.familienname+" erfolgreich angemeldet"}; 514 logger.debug("LOGIN.COMPLETE."+result.body); 515 this.callback(null,result); 516 } 517 }, 518 // step 3: password changed, now store salt 519 function(err,result) { 520 if (err) { 521 var nerr = new Error("LOGIN.ERROR.Error storing changed password: "+err.message); 522 logger.error(nerr.message); 523 this.callback(nerr,result); 524 return; 525 } 526 this.sudbr = new db.Request({info: "Store new password salt for "+this.req.session.username, 527 reqid: 'RQID_STOPWDSALT', 528 sql: "MERGE INTO CCATT pws "+ 529 " USING (SELECT :USERNAME as ATTPARKURZ, :PASSWORD_SALT as ATTVALUE from dual) pwu "+ 530 " ON (pws.ATTARCHTYP='USPWD' and pws.ATTPARTYPE='CCUS' and pws.ATTPARKURZ=pwu.ATTPARKURZ and pws.ATTNAME='PASSWORD_SALT') "+ 531 " WHEN MATCHED THEN UPDATE SET pws.ATTVALUE = pwu.ATTVALUE "+ 532 " WHEN NOT MATCHED THEN INSERT (ATTARCHTYP,ATTPARTYPE,ATTPARKURZ,ATTNAME,ATTVALUE) "+ 533 " VALUES('USPWD','CCUS',pwu.ATTPARKURZ,'PASSWORD_SALT',:PASSWORD_SALT)", 534 params: [this.req.session.username, this.password_salt]}); 535 db.select(this.sudbr,this); 536 }, 537 // step 4: salt store complete 538 function(err,result) { 539 if (err) { 540 var nerr = new Error("Error storing password salt: "+err.message); 541 logger.error(nerr.message); 542 this.callback(nerr,result); 543 return; 544 } 545 var result = {resulttype: 'string', body:this.req.session.vorname+" "+this.req.session.familienname+" erfolgreich angemeldet, Kennwort gesetzt"} 546 logger.debug("LOGIN.COMPLETE.PASSWORD_SET."+result.body); 547 this.callback(null,result); 548 } 549 ); 550 551 /* ***************************************************************************** 552 * FLOW: do_logout 553 * INPUT: sess - session object 554 * CALLBACK: rfunc - callback function to be called upon completion 555 * PARAMETERS: 556 * err - error object if an error occured, otherwise null 557 * result - result object: 558 * body - text with information for user logged out 559 */ 560 var do_logout = flow.define( 561 // step 1: destroy credentials in session 562 function(sess,rfunc) { 563 this.session = sess; 564 this.callback = rfunc; 565 if (!sessionIsLoggedIn(this.session)) { 566 aux.error_callback(this.callback,null,'nicht angemeldet (<a href="/login">anmelden</a>)'); 567 return; 568 } 569 this.username = this.session.username; 570 this.vorname = this.session.vorname; 571 this.familienname = this.session.familienname; 572 delete this.session.username; 573 delete this.session.vorname; 574 delete this.session.familienname; 575 delete this.session.rightslist; 576 this(null,{resulttype: 'string', body:'Session für Benutzername '+this.username+', '+this.vorname+' '+this.familienname+' abgemeldet'}); 577 }, 578 // step 2: complete, prepare success result object 579 function(err,result) { 580 if (err) { 581 aux.error_callback(this.callback,err,'Fehler bei Abmeldung',result); 582 return; 583 } 584 logger.debug("LOGOUT.COMPLETE."+result.body); 585 logout_deregister_session(this.session); 586 this.callback(null,result); 587 }); 588 589 /* ***************************************************************************** 590 * ROUTE: /logout 591 * ACTION: logs session out, destroys all session credentials 592 */ 593 app.get("/logout", 594 function(req,res) { 595 if (sessionIsLoggedIn(req.session)) { 596 do_logout(req.session, 597 function(err,result) { 598 if (err) { 599 res.render('error',{errmsg: err.message}); 600 return; 601 } 602 res.redirect(303,'/login'); 603 }); 604 } else { 605 res.redirect(303,'/login'); 606 } 607 }); 608 609 /* ***************************************************************************** 610 * 611 * ROUTE: /dbconfig 612 * ACTION: presents database selection page 613 */ 614 app.get("/dbconfig", 615 function(req,res) { 616 var rd = {}; 617 rd.dbnames = db.listConfigs(); 618 res.render('dbselect',rd); 619 } 620 ); 621 622 /* ***************************************************************************** 623 * 624 * ROUTE: /do_dbconfig 625 * ACTION: performs database selection upon input from /dbconfig 626 */ 627 app.post('/do_dbconfig',function(req,res) { 628 var form = new formidable.IncomingForm(); 629 form.keepExtensions = true; 630 form.parse(req, 631 function(err, fields, files){ 632 if(err) { 633 var nerr = new Error("Error in parsing database select form: "+err.message); 634 logger.error(nerr.message); 635 res.render('error',{errmsg: nerr.message}); 636 } 637 var dbn = fields.dbname; 638 do_dbconfig(req.session,dbn, 639 function(err,result) { 640 if (err) { 641 var nerr = new Error("Error in DO_DBCONFIG(\""+dbn+"\"): "+err.message); 642 logger.error(nerr.message); 643 res.render('error',{errmsg: nerr.message}); 644 return; 645 } 646 res.redirect(303,'/about'); 647 }); 648 }); 649 }); 650 651 /* **************************************************************************** 652 * FLOW: do_dbconfig 653 * INPUT: SESS - session object for checking authorization credentials 654 * DBN - name of new database configuration to use 655 * CALLBACK: RFUNC - called upon completion 656 * PARAMETERS: 657 * ERR - error object, undefined of everything OK 658 * RESULT - result object 659 */ 660 661 var do_dbconfig = flow.define( 662 // step 1: check authorization 663 function(sess,dbn,rfunc) { 664 this.session = sess; 665 this.dbn = dbn; 666 this.callback = rfunc; 667 functions.check_authorization(this.session,'do_dbconfig',this,this); 668 }, 669 // step 2: perform database configuration switch 670 function(err,result) { 671 if (err) { 672 var nerr = new Error("Authorization Error in DO_DBCONFIG(\""+this.dbn+"\"): "+err.message); 673 logger.error(nerr.message); 674 this.callback(nerr,result); 675 return; 676 } 677 var restext = db.setConfig(dbn); 678 this.callback(undefined,{info: restext}); 679 } 680 // end of flow do_dbconfig 681 ); 682 683 /* ***************************************************************************** 684 * 685 * ROUTE: /upload 686 * ACTION: present a form for file upload 687 */ 688 app.get('/upload', function(req, res) { 689 res.render('upload',{body: ""}); 690 }); 691 692 /* ***************************************************************************** 693 * ROUTE: /do_upload 694 * ACTION: perform upload 695 */ 696 app.post('/do_upload', 697 function(req, res) { 698 var form = new formidable.IncomingForm(); 699 form.uploadDir = prefs.uploaddir; 700 form.keepExtensions = true; 701 form.parse(req, function(err, fields, files){ 702 if(err) { 703 var errmsg = "Error in parsing upload form: "+err.message; 704 res.render('error',{errmsg: errmsg}); 705 } 706 var rd = {}; 707 rd.filename = files.upload_file.name; 708 if (files.upload_file.path.startsWith(prefs.appbase)) { // uploaded to application tree directory? 709 rd.filepath = files.upload_file.path.substring(prefs.appbase.length+path.sep.length); // .. yes, store relative, (hopefully normalized) path 710 } else { 711 rd.filepath = files.upload_file.path; // .. no, store absolute, complete (most probably platform-specific!) path 712 } 713 rd.mfileid = fields.filetype+'_'+aux.nowstring(); 714 rd.mfiletype= fields.filetype; 715 rd.mfileatt = fields.mfileatt; 716 functions.check_authorization(req.session,'do_upload',req, 717 function(err,result) { 718 if (err) { 719 var nerr = new Error("Authorization error in DO_UPLOAD: "+err.message); 720 logger.error(nerr.message); 721 res.render('error',{errmsg: nerr.message}); 722 return; 723 } 724 var dbr = new db.Request({info: "insert new managed file with id "+rd.mfileid, 725 reqid: 'RQID_INSMFILE', 726 headertext: "Neues Managed File "+rd.mfileid+" angelegt", 727 oracle_sql: "insert into CCMFILES(mfiletype, mfileid, mfilename, mfilepath, mfileattribs) "+ 728 " values(:mfiletype,:mfileid,:mfilename,:mfilepath,:mfileattribs)", 729 params: [rd.mfiletype, rd.mfileid, rd.filename, rd.filepath, rd.mfileatt]}); 730 db.select(dbr, 731 function(err,result) { 732 if (err) { 733 var errmsg = "Error in DO_UPLOAD: "+err.message; 734 logger.error(errmsg); 735 res.render('error',{errmsg: errmsg}); 736 } 737 var rd = { mfiletype: this.params[0], mfileid: this.params[1], mfilepath: this.params[2] }; 738 res.render('upload_done',rd); 739 }.bind(dbr)); 740 }); 741 }); 742 }); 743 744 /* ***************************************************************************** 745 * 746 * ROUTE: /mfiles 747 * ACTION: list managed file 748 */ 749 app.get('/mfiles', function(req, res){ 750 var rd = {}; 751 rd.dbresults = new Array(); 752 var tdi; 753 var tdf; 754 functions.check_authorization(req.session,'mfiles',req, 755 function(err,result) { 756 if (err) { 757 var nerr = new Error("Authorization error in MFILES: "+err.message); 758 logger.error(nerr.message); 759 res.render('error',{errmsg: nerr.message}); 760 return; 761 } 762 var dbr = new db.Request({ info: "List Managed Files", 763 reqid: 'RQID_LISTMFILES', 764 headertext: "Managed Files", 765 sql: 'select mfileid as "Managed File ID",mfiletype as "Managed File Type",'+ 766 ' mfilename as "Ursprünglicher Dateiname",mfilepath as "Speicherpfad",'+ 767 ' mfileattribs as "Zusatz-Attribute (JSON-Format)" '+ 768 ' from CCMFILES order by mfileid asc'}); 769 db.select(dbr, 770 function(err,dbres) { 771 if (err) { 772 var errmsg = "Error in list managed files: "+err.message; 773 res.render('error',{errmsg: errmsg}); 774 return; 775 } 776 rd.dbresults.push(copy_dbresult(dbres,dbr.headertext,dbr.desctext)); 777 res.render('mfiles',rd); 778 }.bind(dbr)); 779 }); 780 }); 781 782 /* ***************************************************************************** 783 * ROUTE: /showfile 784 * ACTION: show properties of one managed file 785 * PARAMETERS: MFILEID - managed file ID of the file to show 786 */ 787 app.get("/showfile", function(req, res) { 788 if (!req.query.hasOwnProperty('mfileid')) { 789 var errmsg = 'Error in SHOWFILE: field "mfileid" not in HTTP-request' 790 logger.error(errmsg); 791 res.render('error',{errmsg: errmsg}); 792 } else { 793 mfileinfo(req.session,req.query.mfileid, 794 function(err,result) { 795 if (err) { 796 res.render('error',{errmsg: err.message}); 797 return; 798 } 799 res.render('showfile',result); 800 }) 801 } 802 }); 803 804 /* ***************************************************************************** 805 * FLOW: mfileinfo 806 * INPUT: sess - session object for checking settings and authorization 807 * mfileid - ID of managed file to get info for 808 * CALLBACK: rfunc 809 * parameters: 810 * err - indicating error, if no error, undefined 811 * result - information object: 812 * mfiletype - type of managed file 813 * mfilename - original file name 814 * mfilepath - full path in local FS 815 * mfileatt - managed file attributes in stringified form (as stored in table CCMFILE) 816 * mfileattribs - object with MFILE attributes 817 * filetype - MIME-Type of file (if available) 818 * filext - filename extension (of original file name) 819 * actionurls - array of request URLS and display texts applicable to this file 820 * DESCRIPTION: information about the managed file <mfileid> is determined as follows: 821 * MFILExxx attributes are got from the CCMFILE-entry 822 * FILETYPE and FILEXT are got from the original file name and the contents of the file 823 * ACTIONURLS are determined based on: 824 * - RW MFTACTS, looking up MFILETYPE 825 * - RW FTACTS, looking up FILETYPE and FILEXT 826 * - contents of MFILEATTRIBS: 827 * -- if MFILETYPE='ccdbtable': 828 * --- MFILEATTRIBS.LOADCONTROLS: 829 * each entry in LOADCONTROLS generates one ACTIONURL for loading the file or one part of the file 830 * into the CCDB table with sub-attribute MFILEATTRIBS.LOADCONTROLS.<controlname>.TABLENAME 831 * --- MFILEATTRIBS.TABLE: 832 * generates one ACTIONURL to load the file or (if this file is a workbook and MFILEATTRIBS.SHEETNAME is present) 833 * one part (worksheet) of the file into the CCDB table with the name from MFILEATTRIBS.TABLE 834 */ 835 var mfileinfo = flow.define( 836 // step 1: check authorization 837 function (sess,mfileid,rfunc) { 838 this.session = sess; 839 this.mfileid = mfileid; 840 this.callback = rfunc; 841 functions.check_authorization(this.session,'mfileinfo',this,this); 842 }, 843 // step 2: get MFILE information 844 function(err,ares) { 845 if (err) { 846 var nerr = new Error("Authorization error in MFILEINFO: "+err.message); 847 logger.error(nerr.message); 848 this.callback(nerr,ares); 849 return; 850 } 851 functions.get_mfile_data(this.mfileid,this); 852 }, 853 // step 3: got MFILE info, initialize structures and get file type 854 function(err,mfileinfo) { 855 if (err) { 856 aux.error_callback(this.callback,err,'Error in MFILEINFO reading MFILE data',mfileinfo); 857 return; 858 } 859 this.rd = {}; 860 this.rd.mfileid = mfileinfo.mfileid; 861 this.rd.mfiletype = mfileinfo.mfiletype; 862 this.rd.mfilename = mfileinfo.mfilename; 863 this.rd.mfilepath = mfileinfo.mfilepath; 864 this.rd.mfileatt = mfileinfo.mfileattribs; 865 if (this.rd.mfileatt) { 866 try { 867 this.rd.mfileattribs = JSON.parse(this.rd.mfileatt); 868 } 869 catch (e) { 870 aux.error_callback(this.callback,e,'MFILEINFO: Error parsing attributes',mfileinfo); 871 return; 872 } 873 } else { 874 this.rd.mfileattribs = {}; 875 } 876 aux.default_param(this.rd,'mfileattribs',{}); 877 this.rd.actionurls = []; 878 aux.getFileType(this.rd.mfilepath,this); 879 }, 880 // step 4: got physical file type, determine possible action to be executed on this file 881 function(err,result) { 882 if (err) { 883 aux.error_callback(this.callback,err,"MFILEINFO: error determining file type of "+this.rd.mfilepath); 884 return; 885 } 886 this.rd.filetype = result.mime; 887 this.rd.filtypext = result.ext; 888 this.rd.filnamext = result.namext; 889 this.rd.filactstat = result.filactstat; 890 this.rd.fexists = result.fexists; 891 this.rd.statstring = JSON.stringify(result.stats,null,2); 892 if ((this.rd && this.rd.mfileattribs && this.rd.mfileattribs.extension)) { 893 this.rd.filattext = this.rd.mfileattribs.extension; 894 } else { 895 this.rd.filattext = 'undef'; 896 } 897 logger.debug("MFILEINFO.MFILEPATH=\""+this.mfilepath+"\".MIME_TYPE=\""+this.rd.filetype+"\".FILETYPEEXTENSION=\""+this.rd.filtypext+ 898 "\".FILNAMEXTENSION=\""+this.rd.filnamext+"\".FILATTEXTENSION=\""+this.rd.filattext+"\""); 899 this.rd.actionurls.push({url:'/delete_mfile?mfileid='+this.rd.mfileid, text:'Managed File löschen'}); 900 // 1) actions specified based upon MFILETYPE in RW MFTACTS or on FILETYPE in RW FTACTS 901 this.fadbr = new db.Request( { info: "Determine MFILE actions for MFILETYPE "+this.rd.mfiletype, 902 reqid: 'RQID_GETMFTACT', 903 headertext: "MFILE actions, MFILETYPE="+this.rd.mfiletype, 904 sql: "select RWTYP,RWPARENT,RWNAME,RWVALUE,RWDESC from CCRW "+ 905 "where (RWTYP='MFTACT' and RWPARENT='MFTACTS' and RWNAME like '"+this.rd.mfiletype+"%') or "+ 906 "(RWTYP='FTACT' and RWPARENT='FTACTS' and RWNAME like '"+this.rd.filetype+"%') or "+ 907 "(RWTYP='FTACT' and RWPARENT='FTACTS' and UPPER(RWNAME) like '"+this.rd.filtypext.toUpperCase()+"%') or "+ 908 "(RWTYP='FTACT' and RWPARENT='FTACTS' and UPPER(RWNAME) like '"+this.rd.filnamext.toUpperCase()+"%') or "+ 909 "(RWTYP='FTACT' and RWPARENT='FTACTS' and UPPER(RWNAME) like '"+this.rd.filattext.toUpperCase()+"%')", 910 params: []}); 911 db.select(this.fadbr,this); 912 }, 913 // step 5: got actions for the MFILETYPE and the FILETYPE from CCRW, RW MFTACTS and FTACTS 914 function(err,result) { 915 if (err) { 916 var l3err = new Error("MFILEINFO: error getting MFACTIONS for type "+this.rd.mfiletype+": "+err.message); 917 logger.error(l3err.message); 918 this.callback(l3err,result); 919 return; 920 } 921 // 1) each row in the RW MFTACTS/FTACTS is an entry for an action: 922 // column 3 - RWVALUE contains an URL with placeholders 923 // column 4 - RWDESC contains the text to be displayed for this action 924 var url, text; 925 for (var fai=0; fai<result.rows.length; fai++) { 926 url = aux.populate(result.rows[fai][3],this.rd); 927 text = aux.populate(result.rows[fai][4],this.rd); 928 this.rd.actionurls.push({url:url,text:text}); 929 } 930 // another source for actions are the file attributes: 931 // 2) the 'loadcontrols' specify a set of loading actions: 932 // 'tablename' specifies a table to be loaded 933 // optionally 'sheetname' specifies the worksheet to be loaded 934 // 3) alternatively, the file (or sheet) can be displayed 935 if ((this.rd.mfileattribs) && (this.rd.mfileattribs.loadcontrols)) { 936 for (var tabnam in this.rd.mfileattribs.loadcontrols) { 937 if (this.rd.mfileattribs.loadcontrols[tabnam].tablename) { 938 var url = '/dquery?dataname=LOADTABLE&mfileid='+this.rd.mfileid 939 +'&tablename='+this.rd.mfileattribs.loadcontrols[tabnam].tablename; 940 var text = 'Lade CCDB Tabelle '+this.rd.mfileattribs.loadcontrols[tabnam].tablename+" aus "; 941 if (this.rd.mfileattribs.loadcontrols[tabnam].sheetname) { 942 url += '&sheetname='+this.rd.mfileattribs.loadcontrols[tabnam].sheetname; 943 text += 'dem Blatt '+this.rd.mfileattribs.loadcontrols[tabnam].sheetname+" "; 944 } 945 text += 'dieser Datei'; 946 this.rd.actionurls.push({url: url,text: text}); 947 url = 'dquery?dataname=SHOWXLS&mfileid='+this.rd.mfileid 948 +'&tablename='+this.rd.mfileattribs.loadcontrols[tabnam].tablename; 949 text = 'Zeige '; 950 if (this.rd.mfileattribs.loadcontrols[tabnam].sheetname) { 951 url += '&sheetname='+this.rd.mfileattribs.loadcontrols[tabnam].sheetname; 952 text += 'das Blatt '+this.rd.mfileattribs.loadcontrols[tabnam].sheetname+' dieser Datei '; 953 } else { 954 text += 'diese Datei '; 955 } 956 text += 'an'; 957 this.rd.actionurls.push({url: url, text: text}); 958 } 959 } 960 } 961 // 4) if the MFILE has a 'table' attribute, an action for loading the table form the file is shown 962 // optionally, a 'sheetname' can be specified as MFILE-attribute 963 if ((this.rd.mfileattribs) && (this.rd.mfileattribs.table)) { 964 var url = '/dquery?dataname=LOADTABLE&mfileid='+this.rd.mfileid+'&tablename='+this.rd.mfileattribs.table; 965 var text = 'Lade CCDB Tabelle '+this.rd.mfileattribs.table+' aus '; 966 if (this.rd.mfileattribs.sheetname) { 967 url += '&sheetname='+this.rd.mfileattribs.sheetname; 968 text += 'dem Blatt '+this.rd.mfileattribs.sheetname+' '; 969 } 970 text += 'dieser Datei'; 971 this.rd.actionurls.push({url: url, text: text}); 972 } 973 this.callback(undefined,this.rd); 974 }); 975 976 /* ***************************************************************************** 977 * ROUTE: /delete_mfile 978 * ACTION: removes the selected managed file 979 */ 980 app.get('/delete_mfile', 981 function (req,res) { 982 if (!req.query.hasOwnProperty('mfileid')) { 983 var errmsg = 'DELETE_MFILE: field "mfileid" not in HTTP-request'; 984 res.render('error',{errmsg: errmsg}); 985 return; 986 } else { 987 delete_mfile(req.session,req.query.mfileid, 988 function(err,result) { 989 if (err) { 990 var errmsg = 'Error in DELETE_MFILE: '+err.message; 991 logger.error(errmsg); 992 res.render('error',{errmsg: errmsg}); 993 return; 994 } 995 res.render('info',result); 996 }); 997 } 998 }); 999 1000 /* ***************************************************************************** 1001 * FLOW: delete_mfile 1002 * INPUT: MFILE - managed file ID to delete 1003 * SESS - session object to determine authorization 1004 * CALLBACK: RFUNC - called upon completion 1005 * PARAMETERS: 1006 * ERR - error object, null if everything OK 1007 * RESULT - result information 1008 */ 1009 var delete_mfile = flow.define( 1010 // step 1: check authorization 1011 function(sess,mfileid,rfunc) { 1012 this.session = sess; 1013 this.mfileid = mfileid; 1014 this.callback = rfunc; 1015 functions.check_authorization(this.session,'delete_mfile',this,this); 1016 }, 1017 // step 2: get MFILE data 1018 function(err,result) { 1019 if (err) { 1020 aux.error_callback(this.callback,err,'Authorization error in DELETE_MFILE',result); 1021 return; 1022 } 1023 functions.get_mfile_data(this.mfileid,this); 1024 }, 1025 // step 2: delete CCMFILES entry 1026 function(err,mfileinfo) { 1027 if (err) { 1028 aux.error_callback(this.callback,err,'Error getting MFILE data for '+this.mfileid,mfileinfo); 1029 return; 1030 } 1031 this.mfileattr = mfileinfo.mfileid; 1032 this.mfiletype = mfileinfo.mfiletype; 1033 this.mfilename = mfileinfo.mfilename; 1034 this.mfilepath = mfileinfo.mfilepath; 1035 this.mfileatt = mfileinfo.mfileattribs; 1036 var dbr = new db.Request({ info: 'delete managed file '+this.mfileid, 1037 reqid: 'RQID_DELMFILE', 1038 sql: 'delete from CCMFILES where mfileid=:mfileid', 1039 params: [this.mfileid]}); 1040 db.select(dbr,this); 1041 }, 1042 // step 3: delete physical file 1043 function(err,dbres) { 1044 if (err) { 1045 aux.error_callback(this.callback,err,'Error deleting MFILE data for '+this.mfileid,dbres); 1046 return; 1047 } 1048 fs.unlink(this.mfilepath,this); 1049 }, 1050 // step 4: complete 1051 function(err) { 1052 if (err) { 1053 aux.error_callback(this.callback,err,'Error in deleting managed file '+this.mfileid+', physical file '+this.mfilepath); 1054 return; 1055 } 1056 this.callback(undefined,{ title: 'Managed File löschen', 1057 info: 'Managed File <code>'+this.mfileid+'</code> gelöscht'}); 1058 }); 1059 1060 function isNumericDigit(d) { 1061 return (d>='0' && d<='9'); 1062 } 1063 1064 /* ***************************************************************************** 1065 * FUNCTION: fold_form_fields_to_query 1066 * INPUT: qry - query object to fold the form fields into 1067 * flds - object with attributes like <dataname>_form.<parameter> 1068 * 1069 * DESCRIPTION: copies all form data fields from <flds> as attributes to <qry> 1070 * the fields are expected to be named like <dataname>_form.<parameter> 1071 * if form fields are more complex, they will have multi-segment <parameter>-names 1072 * in this case, hierarchical objects will be built in <qry> 1073 * as a special case, if <parameter> or segment names are numeric, an array 1074 * will be built 1075 * field names are checked, if they belong to the same form, they must have the same <dataname> 1076 * errors are logged, if any checks are violated 1077 */ 1078 function fold_form_fields_to_query(qry,flds) { 1079 var parts, d, bo; 1080 var p1 = null; 1081 var fc = 0; 1082 for (var fn in flds) { 1083 fc++; 1084 parts = fn.split('.'); 1085 if (p1) { 1086 if (parts[0]!=p1) { 1087 logger.error("Form Field "+fn+" has other header field \""+parts[0]+"\" than previous ones (\""+p1+"\")"); 1088 } 1089 } else { 1090 p1 = parts[0]; 1091 } 1092 if (parts.length>1) { // must at least have 2 parts: "<dataname>_form.<parameter>", others are ignored 1093 if (parts.length>2) { // <parameter> has more than one part, build hierarchical object from it 1094 d = 1; 1095 bo = qry; // the current base object, lowest level base object is the query itself 1096 for (d=1;d<(parts.length-1);d++) { // build the path for this very field 1097 if (!bo[parts[d]]) { // current branch already exists ? No, create it 1098 if (isNumericDigit(parts[d+1].charAt(0))) { // if segment name is numeric, ... 1099 bo[parts[d]] = new Array(); // ... create an array ... 1100 } else { 1101 bo[parts[d]] = {}; // ... otherwise, create an object 1102 } 1103 } 1104 bo = bo[parts[d]]; // climb on level up the tree 1105 } 1106 // now add the new leaf 1107 if (util.isArray(bo)) { 1108 bo.push(flds[fn]); // ... either as an array element ... 1109 } else { 1110 bo[parts[parts.length-1]] = flds[fn]; // ... or as a new attribute on the highest branch 1111 } 1112 } else { 1113 qry[parts[1]] = flds[fn]; // if no hierarchical object, simply ann a new attribute to the query 1114 } 1115 } else { 1116 logger.error("Form field "+fn+" has no header field"); 1117 } 1118 } 1119 // make some plausibility checks 1120 if (fc<1) return; // .. but only if any fields were there to be processed 1121 if (!qry.dataname) { 1122 logger.error("query form data does not have dataname parameter"); 1123 } else { 1124 if (!p1) { 1125 logger.error("param form does not have any header field:"); 1126 console.log("QUERY: ",qry); 1127 console.log("FIELDS: ",flds); 1128 } else { 1129 var hfp = p1.split("_"); 1130 if (hfp[0]!=qry.dataname) { 1131 logger.error("query header field dataname \""+qry.dataname+"\" is not equal form data name of \""+hfp[0]+"\""); 1132 } 1133 if (hfp[1]!="form") { 1134 logger.error("form data header field name \""+p1+"\" does not end with _form"); 1135 } 1136 } 1137 } 1138 } 1139 1140 /* ***************************************************************************** 1141 * 1142 * ROUTE: /dquery 1143 * ACTION: executes a query according to the query descriptor, processed by flow DQUERY 1144 */ 1145 app.all("/dquery",function(req,res) { 1146 var form = new formidable.IncomingForm(); 1147 form.uploadDir = __dirname + "/temp_uploads"; 1148 form.keepExtensions = true; 1149 form.parse(req, 1150 function(err,fields,files) { 1151 if (err) { 1152 var nerr = new Error("Error parsing form: "+err.message); 1153 res.render('error',{errmsg: errmsg}); 1154 return; 1155 } 1156 fold_form_fields_to_query(req.query,fields); 1157 aux.default_param(req.query,'dataname','NO_DATANAME_SPECIFIED'); 1158 aux.default_param(req.query,'puth',20); // per default, push action to the background after 20 progress events 1159 var pucnt = 0; 1160 var action_stored = false; 1161 var action_handle = null; 1162 // logger.debug("/DQUERY.FIELDS:",fields); 1163 dquery(req.session,req.query, 1164 // completion callback 1165 function(err,result) { 1166 if (err) { 1167 if ((result) && (result.resulttype) && (result.resulttype=='paramspec')) { // special 'error': need user input, result is a paramspec 1168 result.url = '/dquery'; 1169 result.reqtype = 'Aktion'; 1170 prepare_param_form(result, 1171 function(err,formdata) { 1172 if (err) { 1173 var errmsg = "Error in preparing query form: "+err.message; 1174 logger.error(errmsg); 1175 res.render('error',{errmsg: errmsg, data: formdata}); 1176 return; 1177 } 1178 aux.escapeSingleQuoteAllStrings(formdata); 1179 aux.escapeDoubleQuoteAllStrings(formdata); 1180 aux.replaceControlWhitespaceAllStrings(formdata); 1181 var jsondata = JSON.stringify(JSON.decycle(formdata)); 1182 var jsonundecycled = JSON.stringify(formdata) 1183 // formdata.jsondata = jsondata; 1184 formdata.jsondata = jsonundecycled; 1185 aux.default_param(formdata,'formview','queryparamform'); 1186 res.render(formdata.formview,formdata); 1187 return; 1188 }); 1189 return; 1190 } 1191 var nerr = new Error("Error in DQUERY: "+err.message); 1192 logger.error(nerr.message); 1193 if (action_stored) { 1194 global_active_actions[action_handle].result = result; 1195 global_active_actions[action_handle].error = nerr; 1196 global_active_actions[action_handle].status = 'error'; 1197 return; 1198 } else { 1199 res.render('error',{errmsg: nerr.message, result: result}); 1200 return; 1201 } 1202 } 1203 // action is complete, no error 1204 aux.default_param(result,'disposition','view'); 1205 if (result.decoration) { 1206 // logger.debug("/DQUERY: result decoration specified:"); 1207 // logger.debug(result.decoration); 1208 decorate(result); 1209 } 1210 store_result(req.session,result); 1211 if (action_stored) { // action is stored away, update its status there 1212 global_active_actions[action_handle].result = result; // keep result, is already decorated, but do not yet render it 1213 global_active_actions[action_handle].status = 'complete'; 1214 } else { // action still in foreground of session, render its result to the client 1215 if (result.disposition=='view') { // configured to render result with a view 1216 aux.default_param(result,'view','plain'); // default view is 'plain' (only has one placeholder for attribute 'body') 1217 res.render(result.view,result); 1218 } else { // all other dispositions lead here, result attribute 'data' is directly sent to client 1219 aux.default_param(result,'contentType','text/html'); 1220 res.type(result.contentType); 1221 res.status(200); 1222 res.send(result.data); 1223 } 1224 } 1225 }, 1226 // progress status callback 1227 function(progstat) { 1228 var pstat = {action: '/dquery', 1229 stage: '1/1 - dquery', 1230 subaction: progstat}; 1231 // logger.debug("/dquery.PROGRESS:"); 1232 // logger.debug(pstat); 1233 if (action_stored) { 1234 update_object(global_active_actions[action_handle],pstat); 1235 return; 1236 } 1237 pucnt++; 1238 if (pucnt>req.query.puth) { // if update cycle threshhold exceeded ... 1239 store_active_action(req.session,pstat); // ... push to background: save the action status ... 1240 action_handle = pstat.actid; // keep reference to the stored action so that the completion callback can use it 1241 action_stored = true; 1242 res.redirect(303,'/show_active_action?actid='+pstat.actid); // ... and redirect user to action status page 1243 } else { 1244 res.connection.setTimeout(0); // this could take a while, so do not let browser time out 1245 } 1246 }); 1247 }); 1248 }); 1249 1250 /* ***************************************************************************** 1251 * ROUTE: /load_dcresweekly 1252 * ACTION: load a managed file containing weekly datacenter resource usage data into table CCDCRESL and then CCDCRES after checks 1253 * PARAMETERS: MFILEID - managed file ID of a CSV file to load 1254 */ 1255 app.get("/load_dcresweekly", 1256 function(req, res) { 1257 if (!req.query.hasOwnProperty('mfileid')) { 1258 logger.error('field "mfileid" not in HTTP-request'); 1259 res.redirect(303,'/error'); 1260 } else { 1261 functions.load_dcresweekly(req.session,req.query.mfileid, 1262 function(err,result) { 1263 if (err) { 1264 logger.error(err.message); 1265 res.render('error',{errmsg: err.message, result: result}); 1266 return; 1267 } 1268 res.render('table',result); 1269 }); 1270 } 1271 }); 1272 1273 /* ***************************************************************************** 1274 * FUNCTION: store_result 1275 * INPUT: session - session object 1276 * result - result object 1277 * DESCRIPTION: stores the <result> in GLOBAL_STORED_RESULTS with a unique ID and assigns attribute saveparams.resid 1278 * the ID is also stored in <session>.RESULTS 1279 * 1280 */ 1281 function store_result(session,result) { 1282 var resid = "GSR_"+aux.nowstring(); 1283 result.saveparams = {resid: resid}; 1284 if (session.last_result) { // unlink the previous result from the global store if not yet saved 1285 delete global_stored_results[session.last_result]; 1286 } 1287 if (!global_stored_results) global_stored_results = {}; 1288 global_stored_results[resid] = {}; 1289 global_stored_results[resid].resid = resid 1290 global_stored_results[resid].result = result; 1291 if (!session.results) session.results = {}; 1292 session.last_result = resid; 1293 session.results[resid] = {resid: resid}; 1294 } 1295 1296 /* ***************************************************************************** 1297 * ROUTE: save_result 1298 * ACTION: saves a result semi-permanentely in the session 1299 */ 1300 app.get('/save_result', 1301 function(req,res) { 1302 if (!req.query.hasOwnProperty('resid')) { 1303 logger.error("Request-Feld \"resid\" nicht im HTTP-request"); 1304 res.redirect(303,"/error"); 1305 return; 1306 } 1307 if (!req.session) { 1308 res.render('error',{errmsg: "Ergebnis \""+req.query.resid+"\" kann nicht gespeichert werden, keine Session verfügbar"}); 1309 return; 1310 } 1311 if (!req.session.saved_results) { 1312 req.session.saved_results = {}; 1313 } 1314 if (req.session.last_result && req.session.last_result==req.query.resid) { // unlink the session from the saved result 1315 delete req.session.last_result; 1316 } 1317 req.session.saved_results[req.query.resid] = {resid: global_stored_results[req.query.resid].resid}; 1318 res.render('plain',{body: "Ergebnis \""+req.query.resid+"\" <a href=\"/saved_results\">gespeichert</a>"}); 1319 }); 1320 1321 /* ***************************************************************************** 1322 * ROUTE: saved_results 1323 * ACTION: list saved results for this session 1324 */ 1325 app.get('/saved_results', 1326 function(req,res) { 1327 if (req.session && req.session.saved_results) { 1328 result = new db.Result(); 1329 for (rn in req.session.saved_results) { 1330 result.rows.push(['<a href="/show_result?resid='+rn+'">'+rn+' - '+global_stored_results[rn].result.title+'</a> - '+ 1331 '<a href="/export_result?resid='+rn+'">exportieren</a> - '+ 1332 '<a href="/export_result?resid='+rn+'&mfile">in Managed File exportieren</a> - '+ 1333 '<a href="/show_result?resid='+rn+'&format=json">JSON anzeigen</a>']); 1334 } 1335 result.title='Gespeicherte Abfrage-Ergebnisse'; 1336 res.render('slist',result); 1337 } else { 1338 res.render('plain',{body: 'keine gespeicherten Abfrage-Ergebnisse'}); 1339 } 1340 } 1341 ); 1342 1343 /* ***************************************************************************** 1344 * ROUTE: all_saved_results 1345 * ACTION: list all saved results for all sessions 1346 */ 1347 app.get('/all_saved_results', 1348 function(req,res) { 1349 if (global_stored_results) { 1350 result = new db.Result(); 1351 for (rn in global_stored_results) { 1352 result.rows.push(['<a href="/show_result?resid='+rn+'">'+rn+' - '+global_stored_results[rn].result.title+'</a> - '+ 1353 '<a href="/export_result?resid='+rn+'">exportieren</a> - '+ 1354 '<a href="/export_result?resid='+rn+'&mfile">in Managed File exportieren</a>']); 1355 } 1356 result.title='Gespeicherte Abfrage-Ergebnisse'; 1357 res.render('slist',result); 1358 } else { 1359 res.render('plain',{body: 'keine gespeicherten Abfrage-Ergebnisse'}); 1360 } 1361 } 1362 ); 1363 1364 /* ***************************************************************************** 1365 * ROUTE: show_result 1366 * ACTION: render the saved result <resid> again 1367 */ 1368 app.get('/show_result', 1369 function(req,res) { 1370 if (!req.query.hasOwnProperty('resid')) { 1371 logger.error("Request-Feld \"resid\" nicht im HTTP-request"); 1372 res.redirect(303,"/error"); 1373 return; 1374 } 1375 if (req.query.hasOwnProperty('format') && req.query.format=='json') { 1376 res.render('plain',{body: '<code>'+JSON.stringify(JSON.decycle(global_stored_results[req.query.resid].result))+'</code>'}); 1377 } else { 1378 res.render(global_stored_results[req.query.resid].result.view,global_stored_results[req.query.resid].result); 1379 } 1380 } 1381 ); 1382 1383 /* ***************************************************************************** 1384 * ROUTE: export_result 1385 * ACTION: export the saved result <resid> to a JSON file 1386 */ 1387 app.get('/export_result', 1388 function(req,res) { 1389 if (!req.query.hasOwnProperty('resid')) { 1390 logger.error("Request-Feld \"resid\" nicht im HTTP-request"); 1391 res.redirect(303,"/error"); 1392 return; 1393 } 1394 var resobj = JSON.decycle(global_stored_results[req.query.resid].result); 1395 var resdata = JSON.stringify(resobj); 1396 if (req.query.hasOwnProperty('mfile')) { 1397 functions.save_to_mfile('savedres',resdata,{ extension: 'json', 1398 resid: req.query.resid, 1399 title: resobj.title}, 1400 function(err,result) { 1401 if (err) { 1402 var nerr = 'Error saving result '+resid+' to managed file: '+err.message; 1403 res.render('error',{errmsg: nerr.message}); 1404 return; 1405 } 1406 res.render('nvlist',object2nvlist(result, 1407 { title: 'Ergebnis als Managed File gespeichert', 1408 info: 'Ergebnis '+req.query.resid+' in managed file '+result.mfileid+' gespeichert'})); 1409 }); 1410 } else { 1411 aux.default_param(global_stored_results[req.query.resid].result,'contentType','text/json-decycled'); 1412 res.type(global_stored_results[req.query.resid].result.contentType); 1413 res.status(200); 1414 res.send(resdata); 1415 } 1416 }); 1417 1418 /* ****************************************************************************** 1419 * ROUTE: active_actions 1420 * ACTION: list all active actions for this session 1421 */ 1422 app.get('/active_actions', 1423 function(req,res) { 1424 if (req.session && req.session.active_actions) { 1425 result = new db.Result(); 1426 for (rn in req.session.active_actions) { 1427 if (global_active_actions[rn]) { 1428 result.rows.push(['<a href="/show_active_action?actid='+rn+'">'+rn+' - '+global_active_actions[rn].title+'</a> - '+ 1429 '<a href="/show_active_action?actid='+rn+'&status">Status</a>']) 1430 } else { 1431 logger.error("DATA-STRUCTURE-ERROR: Session-local active action with ACTID=\""+rn+"\" not present in global active actions"); 1432 } 1433 } 1434 result.title='Laufende Aktionen'; 1435 res.render('slist',result); 1436 } else { 1437 res.render('plain',{body: 'keine laufenden Aktionen'}); 1438 } 1439 } 1440 ); 1441 1442 /* ***************************************************************************** 1443 * FUNCTION: store_active_action 1444 * INPUT: session - the session the action is to be stored for 1445 * action - the action object to store 1446 * DESCRIPTION: stores <action> in the global list of active actions 1447 * with a reference in <session> 1448 */ 1449 function store_active_action(session,action) { 1450 var actid = "ACTA_"+aux.nowstring(); 1451 action.actid = actid; 1452 action.session = session; 1453 action.status = 'stored'; 1454 if (session.last_action) { // unlink the previous action from the global store if not yet saved 1455 delete global_active_actions[session.last_action]; 1456 } 1457 if (!global_active_actions) global_active_actions = {}; 1458 global_active_actions[actid] = {}; 1459 global_active_actions[actid].actid = actid; 1460 global_active_actions[actid].session = session; 1461 global_active_actions[actid].action = action; 1462 if (!session.active_actions) session.active_actions = {}; 1463 session.last_action = actid; 1464 session.active_actions[actid] = {actid: actid}; 1465 } 1466 1467 /* ***************************************************************************** 1468 * ROUTE: show_active_action 1469 * ACTION: show the current state of active action <actid> 1470 */ 1471 app.get('/show_active_action', 1472 function(req,res) { 1473 if (!req.query.hasOwnProperty('actid')) { 1474 if ((session) && (session.last_active_action)) { 1475 req.query.actid = session.last_active_action; 1476 } else { 1477 logger.error("Request-Feld \"actid\" nicht im HTTP-request und auch keine letzte Aktion in der Session"); 1478 res.redirect(303,"/error"); 1479 return; 1480 } 1481 } 1482 if (!global_active_actions[req.query.actid]) { 1483 res.render('error',{errmsg: 'Aktion mit ACTID="'+req.query.actid+'" nicht vorhanden'}); 1484 return; 1485 } 1486 var act = global_active_actions[req.query.actid]; 1487 if ((act.status=='complete') && // action is complete, present final result to client 1488 (!(req.query.hasOwnProperty('status')))) { // ... except user requests status explicitly 1489 if (act.result.disposition=='view') { // configured to render result with a view 1490 aux.default_param(act.result,'view','plain'); // default view is 'plain' (only has one place holder for attribute 'body') 1491 res.render(act.result.view, 1492 act.result); 1493 } else { // all other dispositions lead here, result attribute 'data' is directly sent to client 1494 aux.default_param(act.result,'contentType','text/html'); 1495 res.type(act.result.contentType); 1496 res.status(200); 1497 res.send(act.result.data); 1498 } 1499 } else if (act.status=='error') { // action has completed with error 1500 res.render('error',{errmsg: act.error.message, 1501 result: act.result}); // always render with error view 1502 } else { // action not yet complete, present status to client 1503 var mo = { actid: act.actid, status: act.status, 1504 action: act.action, stage: act.stage, 1505 subaction: act.subaction}; // make a copy of the action object to only show specific attributes 1506 if (act.stateunit) mo.stateunit = act.stateunit; // show stateunit only if present in original object 1507 if (act.state) mo.state = act.state; // the same for state 1508 var fact = flatten_object(mo,6,null, // convert to flat NVP object 1509 function(oref) { 1510 var rf; 1511 if (oref.name=="password") { // obscure password values 1512 oref.value = "******"; 1513 rf = true; 1514 } else if ((typeof(oref.value)==='string') && (oref.value.startsWith("[hidden"))) { // suppress hidden values 1515 rf = false; 1516 } else { 1517 rf = true; 1518 } 1519 // logger.debug("SAAFOF: "+oref.name+"=\""+oref.value+"\" - RESULT = "+rf); 1520 return rf; 1521 }); 1522 var farr = object2NVPArray(fact); 1523 res.render('active_action',{actid: fact.actid, actiondata: farr}); 1524 } 1525 } 1526 ); 1527 1528 app.get('/error', 1529 function(req,res) { 1530 var rd = {}; 1531 rd.body='<font size="+5" color=red>Ein Fehler ist aufgetreten</font>'; 1532 res.render('plain',rd); 1533 }); 1534 1535 app.get('/about', 1536 function(req, res){ 1537 var dbcfg = db.getCurrentConfig(); 1538 var rh = "<h2>About CCDB</h2>\n"; 1539 rh += "<table>\n"; 1540 rh += "<tr><td>Script Path:</td><td><code>"+__filename+"</code></td></tr>\n"; 1541 rh += "<tr><td>Script Version:</td><td><code>"+script_version+"</code></td></tr>\n"; 1542 rh += "<tr><td>Database Connect String:</td><td><code>"+dbcfg.getConnectString()+"</code></td></tr>\n"; 1543 rh += "<tr><td>Database User:</td><td><code>"+dbcfg.getUser()+"</code></td></tr>\n"; 1544 rh += "</table>"; 1545 if (req.session) { 1546 rh += "<h2>Session-Information</h2>\n"; 1547 rh += "<table>\n"; 1548 for (so in req.session) { 1549 if (req.session.hasOwnProperty(so)) { 1550 if (typeof req.session[so] === 'object') { 1551 rh += "<tr><td>"+so+"</td><td><code>"+JSON.stringify(JSON.decycle(req.session[so]))+"</code></td></tr>\n"; 1552 } else { 1553 rh += "<tr><td>"+so+"</td><td><code>"+req.session[so]+"</code></td></tr>\n"; 1554 } 1555 } 1556 } 1557 rh += "</table>"; 1558 } 1559 rh += "<br/><a href=\"/dbconfig\">Datenbank auswählen</a>"; 1560 rh += "<br/><a href=\"/shutdown?mode=restart\">CCDB Server neu starten</a>"; 1561 rh += "<br/><a href=\"/shutdown?mode=halt\">CCDB Server beenden</a>"; 1562 rh += "<br/><a href=\"/shutdown?mode=shutdown\">CCDB Server herunterfahren, Neustart verhindern</a>"; 1563 var rd = {body: rh}; 1564 res.render('plain',rd); 1565 }); 1566 1567 // custom 404 page 1568 app.use(function(req, res){ 1569 res.type('text/plain'); 1570 res.status(404); 1571 res.send('404 - Not Found'); 1572 }); 1573 1574 // custom 500 page 1575 app.use(function(err, req, res, next){ 1576 logger.error(err.stack); 1577 res.type('text/plain'); 1578 res.status(500); 1579 res.send('500 - Server Error'); 1580 }); 1581 1582 /* ***************************************************************************** 1583 * 1584 * FLOW: prepare_param_form 1585 * INPUT: paramspec - parameter specification object 1586 * dataname - name of the query the parameter form is for 1587 * sub-form-name if called recursively 1588 * paramdata[] - array with one <paraminfo> per parameter: 1589 * name 1590 * type 1591 * value 1592 * attributes 1593 * source 1594 * CALLBACK: rfunc - called upon completion of preparation 1595 * CALLBACKPARAMS: 1596 * err - error object or undefined if everything OK 1597 * formdata - parameter data for user form presentation: 1598 * substructure: 1599 * paramdata[] - one <paraminfo> entry for each parameter: 1600 * name - name of the parameter 1601 * type - type of the parameter 1602 * value - pre-defined value of the parameter 1603 * desc - description text for the parameter 1604 * vlist - array of available values for this parameter, 1605 * each value has the properties: 1606 * value - value to be passed when selected 1607 * text - text displayed to the user for this value 1608 * desc - description for this value 1609 * ( this attribute only exists for scalar.enum structured parameters ) 1610 * DESCRIPTION: prepare all necessary data to present an input form for all 1611 * required input to the user 1612 * <paramspec> is used to find out which input form fields to prepare 1613 * upon completion, <paramdata> has all elements to render the HTML form 1614 * paramspec has the following structure: 1615 * dataname: name of the query to be performed, 1616 * paramdata: a ROWS object with one row per required input parameter: 1617 * name,type,value,source 1618 * reqtype: name of the query request type: 1619 * usually: 'Abfrage' 1620 * url: the base URL for the query to be executed upon forms completion 1621 * usually: '/dquery' 1622 */ 1623 var prepare_param_form = flow.define( 1624 // step 1: preparations 1625 function(paramspec,rfunc) { 1626 this.paramspec = paramspec; 1627 logger.debug("PREPARE_PARAM_FORM.BEGIN.PARAMSPEC: ",this.paramspec); 1628 this.callback = rfunc; 1629 this.curparnum = 0; // start with 1st parameter 1630 this.types = {}; // collect all types' type-information here 1631 this.paramdata = new Array(); 1632 this.formdata = {}; 1633 for (var psi in this.paramspec) { // copy all input from <paramspec> to <formdata> ... 1634 if (psi!='paramdata') { // ... except <paramspec.paramdata>, which will be ... 1635 this.formdata[psi] = this.paramspec[psi]; // ... prepared by this flow 1636 } 1637 } 1638 // now iterate over all entries in <paramspec.paramdata>, iterator is <curparnum> 1639 this(); 1640 }, 1641 // step 2: process data for parameter <n>, prepare request for parameter <n+1> 1642 function(err,paraminfo) { 1643 if (err) { 1644 var nerr = new Error("Error in preparing form parameter "+this.curparnum+" - \""+this.paramspec.paramdata[this.curparnum][0]+"\": "+err.message); 1645 logger.error(nerr.message); 1646 this.callback(nerr,paraminfo); 1647 return; 1648 } 1649 if (paraminfo) { // process result of parameter <curparnum> 1650 this.paramdata.push(paraminfo); 1651 this.curparnum++; 1652 } 1653 if (this.curparnum<this.paramspec.paramdata.length) { // get info for parameter <curparnum+1> 1654 logger.debug("PREPARE_PARAM_FORM.ITERATE.PARAMSPEC.PARAMDATA["+this.curparnum+"]: ",this.paramspec.paramdata[this.curparnum]); 1655 this.REWIND(); // stay at this step 1656 param_form_query(this.paramspec.paramdata[this.curparnum], 1657 this); // callback from param_form_query() will continue flow 1658 return; // simply return here, flow will be nudged on by callback from param_form_query() 1659 } 1660 this.formdata.paramdata = this.paramdata; 1661 this.callback(null,this.formdata); 1662 return; 1663 } 1664 // end of flow PREPARE_PARAM_FORM 1665 ); 1666 1667 /* ***************************************************************************** 1668 * FLOW: param_form_query 1669 * INPUT: pinfo - paraminfo object: 1670 * name - name of the parameter to prepare paraminfo for 1671 * type - type of the parameter 1672 * value - pre-supplied value for the parameter 1673 * attribs - additional attributes object 1674 * src - source of the requirement for the parameter: 1675 * 'param' - required as DB-query parameter 1676 * 'attrib' - required as general attribute for the query 1677 * 'ptype' - required by <paramtype> query attribute 1678 * CALLBACK: rfunc - called upon completion 1679 * PARAMETERS: 1680 * err - error, if any, otherwise NULL 1681 * paraminfo - a paraminfo object: 1682 * name - name of the query parameter 1683 * type - name of the type 1684 * value - pre-supplied value 1685 * if <type>='array' this is an array of paraminfo 1686 * if <type>='object' this is an object with the attributes (also paraminfos) as defined in the object type definition 1687 * attribs - attributes, as passed from caller 1688 * src - source, as passed from caller 1689 * if <type>='array' this is 'recurse_array' 1690 + IF <type>='object' this is 'recurse_object' 1691 * 1692 * DESCRIPTION: prepares form input information for 1693 * the query parameter in referred to by <name>/<type>: 1694 * 1) get type information from RW TYPES 1695 * 1696 */ 1697 var param_form_query = flow.define( 1698 function (pinfo,rfunc,dep) { 1699 this.callback = rfunc; 1700 this.paraminfo = pinfo; 1701 this.curname = this.paraminfo.name; 1702 this.curtype = this.paraminfo.type; 1703 this.curvalue = this.paraminfo.value; 1704 this.attributes = this.paraminfo.attribs; 1705 this.cursrc = this.paraminfo.src; 1706 this.curdep = dep || 100; // keep track of recursion depth, starting at default of 100 1707 logger.debug("PARAM_FORM_QUERY.BEGIN.DEPTH="+this.curdep+".PINFO: ",pinfo); 1708 this.typdbr = new db.Request({info: "get type info for \""+this.curtype+"\"", 1709 reqid: 'RQID_GETYPINF', 1710 headertext: "type info for \""+this.curtype+"\"", 1711 params: [this.curtype]}); 1712 if (this.typdbr.getDBtype()=="mysql") { // mySQL needs one param for each placeholder in SQL, even if they're the same 1713 this.typdbr.setParams([this.curtype,this.curtype,this.curtype]); 1714 } 1715 db.select(this.typdbr,this); 1716 }, 1717 // step 2: process result of querying type information 1718 function(err,result) { 1719 logger.debug("PARAM_FORM_QUERY.STEP_2.DEPTH="+this.curdep+".PARAMNAME=\""+this.curname+"\".STATE="+this.state+"/"+this.substate); 1720 if (err) { 1721 aux.error_callback(this.callback,err,"Error in param_form_query/GETYPINF",result); 1722 return; 1723 } 1724 if (!(result.rows) || (result.rows.length<1) || (result.rows[0].length<3)) { 1725 aux.error_callback(this.callback,null,"too few or wrong type information data about "+this.curtype+" in param_form_query/GETYPINF",result); 1726 return; 1727 } 1728 // transfer the type attributes to the paraminfo 1729 for (var i=0; i<result.rows.length; i++) { 1730 if (result.rows[i].length<3) { 1731 aux.error_callback(this.callback,null,"too few fields in type definition for \""+this.curtype+"\" row "+i+" in param_form_query/GETYPINF",result); 1732 return; 1733 } 1734 this.paraminfo[result.rows[i][0]] = result.rows[i][1]; 1735 // this.paraminfo[result.rows[i][0]+'_desc'] = result.rows[i][2]; 1736 } 1737 // fold attributes into paraminfo base object, allows overwriting of base type attributes by attributes from query 1738 if (typeof this.attributes !== 'object') { 1739 this.paraminfo.attribs = this.attributes; 1740 } else { 1741 for (var att in this.attributes) { 1742 // logger.debug("...copying attribute "+i+":"); 1743 // logger.debug(this.attributes[i]); 1744 this.paraminfo[att] = this.attributes[att]; 1745 } 1746 } 1747 logger.debug("PARAM_FORM_QUERY.STEP_2.FOLDED_TYPEINFO("+this.curtype+").PARAMINFO: ",this.paraminfo); 1748 aux.default_param(this.paraminfo,'structure','string'); // default structure is 'string' 1749 if (this.paraminfo.structure.indexOf('.')>=0) { 1750 this.paraminfo.substructpath = this.paraminfo.structure.split('.'); 1751 this.paraminfo.structure = this.paraminfo.substructpath[0]; 1752 } 1753 switch (this.paraminfo.structure) { 1754 case 'string': // structure 'string': simple free text string, no more information 1755 this.paraminfo.inputString = true; 1756 aux.default_param(this.paraminfo,'maxlen',40); 1757 aux.default_param(this.paraminfo,'fieldlen',this.paraminfo.maxlen); 1758 this.state = 'got'; 1759 this.substate = 'string'; 1760 this(); // go on to next step, no further query 1761 break; 1762 case 'scalar': 1763 aux.default_param(this.paraminfo,'scalarsubtype','number'); 1764 if (this.paraminfo.substructpath[1]) this.paraminfo.scalarsubtype = this.paraminfo.substructpath[1]; 1765 switch (this.paraminfo.scalarsubtype) { 1766 case 'number': 1767 this.paraminfo.inputNumber = true; 1768 this.paraminfo.paramIsNumber = true; 1769 aux.default_param(this.paraminfo,'minvalue',0); 1770 aux.default_param(this.paraminfo,'maxvalue',-1); 1771 this.state = 'got'; 1772 this.substate = 'number'; 1773 this(); 1774 break; 1775 case 'enum': 1776 case 'presets': 1777 this.dbtype = db.getCurrentConfigDBtype(); 1778 this.paraminfo.inputEnum = (this.paraminfo.scalarsubtype=='enum'); 1779 this.paraminfo.paramIsEnum = (this.paraminfo.scalarsubtype=='enum'); 1780 this.paraminfo.inputPresets = (this.paraminfo.scalarsubtype=='presets'); 1781 this.paraminfo.paramIsPresets = (this.paraminfo.scalarsubtype=='presets'); 1782 if (this.paraminfo.vlistexpr) { // vlistexpr must deliver an Array of objects with attributes value,text,desc 1783 try { 1784 this.paraminfo.vlist = eval(this.paraminfo.vlistexpr); 1785 } 1786 catch (e) { 1787 aux.error_callback(this.callback,e,"Error processing vlistexpr",{resulttype: 'object', resultobject: this.paraminfo}); 1788 return; 1789 } 1790 for (var vi=0; vi<this.paraminfo.vlist.length; vi++) { 1791 aux.default_param(this.paraminfo.vlist[vi],'value',vi); 1792 aux.default_param(this.paraminfo.vlist[vi],'text',this.paraminfo.vlist[vi].value); 1793 aux.default_param(this.paraminfo.vlist[vi],'desc',this.paraminfo.vlist[vi].text); 1794 } 1795 this.state = 'got'; 1796 this.substate = 'vlist'; 1797 this(); 1798 return; 1799 } else if (this.paraminfo["vlistquery_"+this.dbtype]) { // DB-type specific vlistquery must deliver a result with n rows each having 3 columns: value,text,desc 1800 this.dbr = new db.Request({ info: 'VLIST query for parameter name "'+this.curname+'" of type "'+this.curtype+'" on DB-type "'+this.dbtype+'"', 1801 reqid: 'RQID_VLIQRY_'+this.curtype, 1802 sql: this.paraminfo["vlistquery_"+this.dbtype], 1803 params: []}); 1804 this.state = 'wait'; 1805 this.substate = 'vlist'; 1806 db.select(this.dbr,this); 1807 } else if (this.paraminfo.vlistquery) { // general vlistquery must deliver a result with n rows each having 3 columns: value,text,desc 1808 this.dbr = new db.Request({ info: 'VLIST query for parameter name "'+this.curname+'" of type "'+this.curtype+'"', 1809 reqid: 'RQID_VLIQRY_'+this.curtype, 1810 sql: this.paraminfo.vlistquery, 1811 params: []}); 1812 this.state = 'wait'; 1813 this.substate = 'vlist'; 1814 db.select(this.dbr,this); 1815 } 1816 break; 1817 default: 1818 aux.error_callback(this.callback,null,"Error: illegal scalar subtype \""+this.paraminfo.scalarsubtype+"\"",{resulttype: 'object', resultobject: this.paraminfo}); 1819 return; 1820 } 1821 break; 1822 case 'array': // structure 'array': array of <arraytype> 1823 this.paraminfo.inputArray = true; 1824 this.paraminfo.paramIsArray = true; 1825 aux.default_param(this.paraminfo,'arraytype','string'); 1826 aux.default_param(this.paraminfo,'minlength','1'); 1827 aux.default_param(this.paraminfo,'maxlength','50'); 1828 this.paraminfo.value = new Array(); 1829 this.state = 'wait'; 1830 this.substate = 'arraytype' 1831 if (this.paraminfo.paramdata) { 1832 logger.error("PARAMINFO for Array type has paramdata[], this is strange!!"); 1833 } 1834 this.paraminfo.paramdata = new Array(); 1835 param_form_query({ name: this.curname+'_array_row', // get paraminfo for entry type in array, this name must not stay !! 1836 type: this.paraminfo.arraytype, // type of entry 1837 value: this.curvalue, // value of entry, TODO: implement!! 1838 attribs: null, // attributes: not yet known 1839 src: 'recurse_array'}, // source 1840 this, // callback: next step 1841 this.curdep+1); 1842 break; 1843 case 'object': 1844 this.paraminfo.inputObject = true; 1845 this.paraminfo.paramIsObject = true; 1846 aux.default_param(this.paraminfo,'objectattributes','a'); 1847 this.state = 'prep'; 1848 this.substate = 'object'; 1849 this.objectattributes = this.paraminfo.objectattributes.split(','); 1850 if (this.paraminfo.paramdata) { 1851 logger.error("PARAMINFO for Object type has paramdata[], this is strange!!"); 1852 } 1853 this.paraminfo.paramdata = new Array(); 1854 this.curobjattnum = 0; 1855 delete this.paraminfo.value; // object itself must not have value, paramdata[] must carry the field values 1856 this(); // real queries for all attributes are done in next step 1857 break; 1858 default: 1859 aux.error_callback(this.callback,null,"Error: illegal structure name \""+this.paraminfo.structure+"\"",{resulttype: 'object', resultobject: this.paraminfo}); 1860 return; 1861 } 1862 }, 1863 // step 3: got param info query result 1864 function(err,result) { 1865 if (err) { 1866 aux.error_callback(this.callback,err,"Error in param_form_query()/state="+this.state+"/"+this.substate+" for type \""+this.curtype+"\"",result); 1867 return; 1868 } 1869 logger.debug("PARAM_FORM_QUERY.STEP_3.DEPTH="+this.curdep+".PARAMNAME=\""+this.curname+"\".STATE="+this.state+"/"+this.substate); 1870 switch (this.state) { 1871 case 'got': // nothing expected, terminate flow here 1872 this.callback(null,this.paraminfo); 1873 break; 1874 case 'wait': // we're waiting for one of possibly more param query results 1875 if (result) { // if no result, any result is already prepared in this.paraminfo 1876 switch (this.substate) { // what are we waiting for? 1877 case 'vlist': // waiting for paraminfo VLIST 1878 var vle; 1879 this.paraminfo.vlist = new Array(); 1880 for (var vi = 0; vi<result.rows.length; vi++) { 1881 vle = {}; 1882 if (result.rows[vi].length>2) vle.desc = result.rows[vi][2]; 1883 if (result.rows[vi].length>1) vle.text = result.rows[vi][1]; 1884 if (result.rows[vi].length>0) vle.value = result.rows[vi][0]; 1885 aux.default_param(vle,'value',vi); 1886 aux.default_param(vle,'text',vle.value); 1887 aux.default_param(vle,'desc',vle.text); 1888 if (vle.value == this.paraminfo.value) { 1889 vle.selected = true; 1890 } 1891 this.paraminfo.vlist.push(vle); 1892 } 1893 this.callback(null,this.paraminfo); 1894 break; 1895 case 'arraytype': // waiting for type info on array entry type 1896 // result is assumed to be paraminfo 1897 for (var afi = 0; afi<result.paramdata.length; afi++) { 1898 this.paraminfo.paramdata.push(result.paramdata[afi]); // merge the result paramdata[] 1899 } 1900 this.callback(null,this.paraminfo); 1901 break; 1902 case 'object': // waiting for next object attribute paraminfo 1903 // result is assumed to be paraminfo 1904 this.paraminfo.paramdata.push(result); 1905 this.curobjattnum++; // move on to next attribute 1906 this.state = 'prep'; 1907 this.substate = 'object'; 1908 this.REWIND(); 1909 this(); 1910 break; 1911 default: 1912 aux.error_callback(this.callback,null,"Illegal state/substate="+this.state+"/"+this.substate+" in param_form_query()/3/wait"); 1913 return; 1914 } 1915 } else { 1916 aux.error_callback(this.callback,null,"Error in param_form_query().state="+this.state+"/"+this.substate+": got no result"); 1917 return; 1918 } 1919 break; 1920 case 'prep': 1921 switch (this.substate) { 1922 case 'object': 1923 if (this.curobjattnum>=this.objectattributes.length) { // no more object attributes 1924 this.callback(null,this.paraminfo); 1925 } else { 1926 logger.debug("..curobjatt["+this.curobjattnum+"]=\""+this.objectattributes[this.curobjattnum]+"\""); 1927 this.curobjattfields = this.objectattributes[this.curobjattnum].split(':'); 1928 this.curobjattname = this.curobjattfields[0] || this.curobjattnum; 1929 this.curobjatttype = this.curobjattfields[1] || this.curobjattname; 1930 logger.debug("PARAM_FORM_QUERY.STEP_3.CUROBJATTNUM="+this.curobjattnum+".CUROBJATTFIELDS[2]="+this.curobjattfields[2]); 1931 this.curobjattoptstr = aux.populate(this.curobjattfields[2] || '',this.paraminfo,this); 1932 logger.debug("PARAM_FORM_QUERY.STEP_3.CUROBJATTNUM="+this.curobjattnum+".CUROBJATTOPTSTR=\"",this.curobjattoptstr+"\""); 1933 if (this.curobjattoptstr) { 1934 try { 1935 this.curobjattoptions = JSON.parse(this.curobjattoptstr); 1936 } 1937 catch (e) { 1938 aux.error_callback(this.callback,e,"Error JSON-parsing object attributes options for attribute "+this.curobjattname); 1939 return; 1940 } 1941 } else { 1942 this.curobjattoptions = null; 1943 } 1944 logger.debug("PARAM_FORM_QUERY.STEP_3.CUROBJATTNUM="+this.curobjattnum+".CUROBJATTOPTIONS:",this.curobjattoptions); 1945 this.state = 'wait'; 1946 this.substate = 'object'; 1947 this.REWIND(); // stay at this step in another state 1948 param_form_query({name: this.curobjattname, 1949 type: this.curobjatttype, 1950 value: null, // value: TODO: implement!! 1951 attribs: this.curobjattoptions, 1952 src: 'recurse_object_attribute' 1953 }, 1954 this,this.curdep+1); 1955 } 1956 break; 1957 default: 1958 aux.error_callback(this.callback,null,"Illegal state/substate="+this.state+"/"+this.substate+" in param_form_query()/3/prep"); 1959 return; 1960 } 1961 break; 1962 default: 1963 aux.error_callback(this.callback,null,"Illegal state/substate="+this.state+"/"+this.substate+" in param_form_query()"); 1964 return; 1965 } 1966 logger.debug("PARAM_FORM_QUERY.STEP_3.END.PARAM_NAME=\""+this.curname+"\".DEPTH="+this.curdep+".STATE="+this.state+"/"+this.substate); 1967 } 1968 // end of flow PARAM_FORM_QUERY 1969 ); 1970 1971 /* ***************************************************************************** 1972 * FUNCTION: process_sequence_step 1973 * INPUT: that - the sequence execution context 1974 * phase - the name of the phase of the sequence to be processed 1975 * RETURNS: result of the processed expression 1976 * DESCRIPTION: performs pre- or post-processing for the sequence step <that.stepnum> 1977 * This is done by processing a javascript-expression 1978 * The expression is taken: 1979 * - either from the context attribute named <phase>_step_<stepnumber> 1980 * - or from the context attribute named query.<phase>_step_<stepnumber> 1981 * (if first does not exist) 1982 * If neither attributes exist, nothing is done 1983 * The content of the attributes is pre-processed by replacing any placeholders 1984 * delimited by {{..}} (see also aux.populate() ) 1985 * 1986 * 1987 */ 1988 function process_sequence_step(that,phase) { 1989 var procparname = phase+"_step_"+that.stepnum; 1990 var procexpr = ''; 1991 var procres; 1992 var attctx 1993 if (that[procparname]) { 1994 procexpr = aux.populate(''+that[procparname],that); 1995 attctx = 'sequence'; 1996 } else { 1997 if ((that.query) && (that.query[procparname])) { 1998 procexpr = aux.populate(''+that.query[procparname],that); 1999 attctx = 'sequence.query'; 2000 } else { 2001 procexpr = ''; 2002 // logger.debug('SEQUENCE: no JS-expression '+procparname+' defined'); 2003 return; 2004 } 2005 } 2006 logger.debug("SEQUENCE.PROCPAR."+procparname+"(populated)=\""+procexpr+"\""); 2007 var procres; 2008 try { 2009 procres = eval(procexpr); 2010 } 2011 catch(e) { 2012 procres = "*** ERROR processing expression \""+procexpr+"\" from \""+attctx+"."+procparname+"\" ***"; 2013 logger.error(procres); 2014 } 2015 logger.debug("SEQUENCE.RESULT_OF_"+procparname+":"); 2016 logger.debug(procres); 2017 } 2018 2019 /* ***************************************************************************** 2020 * 2021 * FLOW: dsequence 2022 * INPUT: query - query object 2023 * attributes: 2024 * seqname - name of the sequence to execute 2025 * CALLBACK: rfunc - called upon completion of sequence, successsful or in error 2026 * CALLBACKPARAMS: err - error objects, undefined if no error 2027 * result - result object 2028 * attributes: 2029 * rows - result table rows 2030 * data - csv-formatted result, if query.format was 'csv' 2031 * CALLBACK: pfunc - called upon updated progress status 2032 * CALLBACKPARAMS: progstat - arbitrary object describing the progress of the sequence 2033 * 2034 * *****************************************************************************/ 2035 2036 var dsequence = flow.define( 2037 // step 1: check authorization 2038 function(sess,query,rfunc,pfunc) { 2039 this.calldepth = 0; 2040 this.callback = rfunc; 2041 this.pcallback = pfunc; 2042 this.query = query; 2043 this.session = sess; 2044 this.lastresult = null; 2045 this.dbresults = new Array(); 2046 aux.default_param(this.query,'dataname','***UNKNOWN***'); 2047 logger.debug("SEQUENCE.BEGIN.DATANAME=\""+this.query.dataname+"\".FORMAT=\""+this.query.format+"\""); 2048 functions.check_authorization(this.session,'dsequence',this,this); 2049 }, 2050 // step 1a: get sequence descriptor data 2051 function(err,result) { 2052 if (err) { 2053 aux.error_callback(this.callback,err,"Authorization error in SEQUENCE(\""+this.dataname+"\")",result); 2054 return; 2055 } 2056 this.qddbr = new db.Request({sql: "SELECT RWNAME,RWVALUE,RWDESC FROM CCRW WHERE RWTYP='DSEQPARAM' and RWPARENT=:DATANAME", 2057 info: "Get sequence descriptor data for DATANAME=\""+this.query.dataname+"\"", 2058 reqid: 'RQID_GETSEQDESC', 2059 params: [this.query.dataname]}); 2060 db.select(this.qddbr, 2061 this, 2062 function(progstat) { 2063 // logger.debug("DSEQUENCE.GETSEQDESC.PROGRESS:"); 2064 // logger.debug(util.inspect(progstat,false,null)); 2065 this.calldepth++; 2066 var pstat = {action: 'dsequence', stage: 'getquerydesc', calldepth: this.calldepth, subaction: progstat}; 2067 if (this.pcallback) this.pcallback(pstat); 2068 this.calldepth--; 2069 }.bind(this)); 2070 }, 2071 // step 2: query for sequence descriptor completed, check, prepare and execute 2072 function(err,result) { 2073 if (err) { 2074 aux.error_callback(this.callback,err,"Error in DSEQUENCE/GetSeqDesc",result); 2075 return; 2076 } 2077 prepare_dquery_params(this,result); // populate the sequence's parameters, possibly from the sequence descriptor 2078 aux.default_param(this.query,'sequence','EXIT'); 2079 this.steps = this.query.sequence.split(","); // list of steps in the sequence 2080 this.seqlen = this.steps.length; // number of steps in sequence 2081 this.stepnum = 0; // start with 1st step 2082 this(); // proceed to step-processing 2083 }, 2084 // step 3: sequence step completed (if result there), check and then execute next (or first, if no result there) step or terminate (if no (more) steps) 2085 function(err,result) { 2086 if (err) { 2087 aux.error_callback(this.callback,err,"Error in DSEQUENCE/2",result); 2088 return; 2089 } 2090 if (result) { // if result is there, we came from the same state and already processed a step 2091 result.prior = this.lastresult; // build a stack-like chain of results 2092 this.lastresult = result; // attach result to sequence context 2093 this.dbresults.push(result); // collect all results 2094 this.lastresult.dbresults = this.dbresults; // keep a reference to all collected results in each partial result 2095 process_sequence_step(this,'post'); // apply any post-processing defined in 'post_step_<N>' 2096 delete this.stepquery; 2097 this.stepnum++; // proceed with next step 2098 } 2099 this.stepquery = {}; 2100 this.stepquery.sequencecontext = this; // link stepquery back to sequence context to let expressions access it 2101 if (this.stepnum>=this.seqlen) { // sequence is complete, terminate 2102 logger.debug("SEQUENCE.NAME=\""+this.query.dataname+"\".COMPLETE"); 2103 this.callback(undefined,this.lastresult); // the result of the last step is the result of the sequence (post-processing may have amended it) 2104 return; 2105 } else { // sequence not complete, proceed 2106 this.REWIND(); // tell flow to stay at this step 2107 this.curstep = {}; 2108 this.curstep.parts = this.steps[this.stepnum].split(':'); // strip off optional information 2109 this.stepquery.dataname = this.curstep.parts[0]; // get next query dataname (1st part in step string) 2110 this.stepquery.optionstring = (this.curstep.parts.length>1)?this.curstep.parts[1]:''; 2111 if (this.stepquery.optionstring) { 2112 this.stepquery.optionstring = aux.populate(this.stepquery.optionstring,this.query,this); 2113 if (this.stepquery.optionstring.charAt(0)=='{') { 2114 try { 2115 this.stepquery.options = JSON.parse(this.stepquery.optionstring); 2116 } catch (e) { 2117 aux.error_callback(this.callback,e, 2118 "Error JSON-parsing sequence(\""+this.query.dataname+"\")-step("+this.stepnum+",\""+this.stepquer.dataname+"\")-options \""+this.stepquery.optionstring+"\""); 2119 return; 2120 } 2121 } 2122 } 2123 logger.debug("SEQUENCE.NAME=\""+this.query.dataname+"\".STEPNUM="+this.stepnum+".STEP=\""+this.stepquery.dataname+"\""); 2124 process_sequence_step(this,'pre'); // apply any pre-processing defined in 'pre_step_<N+1>' 2125 dquery(this.session,this.stepquery,this, 2126 function(progstat) { 2127 var pstat = {action: 'dsequence', 2128 stage: 'execstep', 2129 stateunit: 'seqstep', 2130 state: this.stepnum, 2131 seqdataname: this.query.dataname, 2132 seqstepquery: this.stepquery, 2133 subaction: progstat 2134 }; 2135 if (this.pcallback) this.pcallback(pstat); 2136 }.bind(this)); // execute next step 2137 } 2138 } 2139 // end of flow dsequence 2140 ); 2141 2142 /* ***************************************************************************** 2143 * FUNCTION: find_name 2144 * INPUT: nvpa - either array of arrays or array of objects 2145 * nam - name to find in <nvpa> 2146 * npn - name of property containing the name (if <nvpa> is an object 2147 * DEFAULT: 'name' 2148 * DESCRIPTION: checks, if an entry with name <nam> already exists in <nvpa> 2149 * if <nvpa> is an array of arrays, the name is assumed to be entry [0] of each sub-array 2150 * if <nvpa> is an array of objects, the name is assumed to be the attribute <npn> of each object 2151 */ 2152 function find_name(nvpa,nam,npn) { 2153 npn = npn || 'name'; 2154 // logger.debug("FIND_NAME.ARRAY:"); 2155 // logger.debug(nvpa); 2156 // logger.debug("FIND_NAME.NAM=\""+nam+"\""); 2157 for (var i=0; i<nvpa.length; i++) { 2158 if (nvpa[i] && util.isArray(nvpa[i])) { 2159 if ((nvpa[i]) && (nvpa[i][0]) && (nvpa[i][0]==nam)) { 2160 return true; 2161 } 2162 } else { 2163 if ((nvpa[i]) && nvpa[i].hasOwnProperty(npn) && nvpa[i][npn]==nam) { 2164 return true; 2165 } 2166 } 2167 } 2168 return false; 2169 } 2170 2171 /* ***************************************************************************** 2172 * FUNCTION: update_object 2173 * INPUT: obj - target object 2174 * updobj - object to update <obj> with 2175 * DESCRIPTION: updates all attributes in <obj> from <updobj> 2176 * if <updobj> has an attribute that <obj> does not have, <obj> will be extended 2177 * if the attribute already exists in <obj>, its value is overwritten from <updobj> 2178 */ 2179 function update_object(obj,updobj) { 2180 for (ua in updobj) { 2181 obj[ua] = updobj[ua]; 2182 } 2183 } 2184 2185 /* ***************************************************************************** 2186 * FUNCTION: flatten_object 2187 * INPUT: obj - hierarchical object 2188 * maxdepth - maximal hierarchical depth to be investigated 2189 * DEFAULT: 1 2190 * nameprefix - prefix to be put in front of each attributes' name in <fobj> 2191 * filterfunc - optional filtering callback function: 2192 * called for each leaf object 2193 * input: objref - pointer to leaf object, possibly modified: 2194 * nameprefix - passed from flatten_object() - invocation 2195 * name - leaf name 2196 * value - leaf value 2197 * result: true - include leaf object in output 2198 * false - omit leaf object from output 2199 * filterfunc may modify nameprefix, name and value of objref 2200 * RETURNS: fobj - flattened object 2201 * DESCRIPTION: makes <fobj> a copy of <obj>, but with all sub-objects folded in 2202 * if an attribute in <obj> is an object itself, its attributes 2203 * are copied down into <fobj> with dotted name 2204 * Example: 2205 * { a: 1, 2206 * b: { b1: 2.1, 2207 * b2: 2.2 } 2208 * c: "maxi" } 2209 * is flattened to: 2210 * { a: 1, 2211 * b.b1: 2.1, 2212 * b.b2: 2.2, 2213 * c: "maxi" } 2214 */ 2215 function flatten_object(obj,maxdepth,nameprefix,filterfunc) { 2216 var fobj = {}; 2217 var tobj = {}; 2218 var tval = null; 2219 for (anam in obj) { 2220 tval = obj[anam]; 2221 try { 2222 aty = typeof tval; 2223 } 2224 catch (e) { 2225 logger.error("FLATTTEN_OBJECT: Error determining type of attribute "+anam+": "+e.message); 2226 aty = 'undetermined' 2227 } 2228 // logger.debug("FLATTEN_OBJECT.MAXDEPTH=\""+maxdepth+"\".NAMEPREFIX=\""+nameprefix+"\".ANAM=\""+anam+"\".ATYPE=\""+aty+"\""); 2229 if (aty === 'object') { // attribute is an object itself, recurse down 2230 if (maxdepth && maxdepth>1) { // but only until <maxdepth> 2231 update_object(fobj,flatten_object(obj[anam],maxdepth-1,''+((nameprefix)?nameprefix:'')+anam+'.',filterfunc)); 2232 continue; 2233 } else { 2234 tval = "[hiddenObject]"; // hide lower hierarchies 2235 } 2236 } 2237 if (aty === 'undetermined') { 2238 tval = "[undetermined]"; // can't grab it ... 2239 } else if (aty === 'function') { 2240 tval = "[hiddenFunction]"; // don't detail functions 2241 } 2242 if (filterfunc) { 2243 tobj.nameprefix = nameprefix; 2244 tobj.name = anam; 2245 tobj.value = tval; 2246 if (filterfunc(tobj)) { 2247 fobj[(tobj.nameprefix)?tobj.nameprefix+tobj.name:tobj.name] = tobj.value; 2248 } 2249 } else { 2250 // logger.debug("FOBJ: no filterfunc - "+nameprefix+"."+anam+" = \""+obj[anam]+"\""); 2251 fobj[(nameprefix)?nameprefix+anam:anam] = tval; 2252 } 2253 } 2254 // logger.debug("FLATTEN_OBJECT.FOBJ:"); 2255 // logger.debug(fobj); 2256 return fobj; 2257 } 2258 2259 /* ***************************************************************************** 2260 * FUNCTION: object2NVPArray 2261 * INPUT: obj - flat object 2262 * RETURNS: array with one entry for each attribute of <obj> 2263 * each entry is an array of 2 elements: 2264 * 0 .. name of the attribute 2265 * 1 .. value of the attribute 2266 */ 2267 function object2NVPArray(obj) { 2268 nvpa = new Array(); 2269 for (anam in obj) { 2270 nvpa.push([anam,obj[anam]]); 2271 } 2272 return nvpa; 2273 } 2274 2275 /* ***************************************************************************** 2276 * FUNCTION: prepare_dquery_params 2277 * INPUT: that - dquery/dsequence context object to be populated from <result> 2278 * result - result object from reading dquery/dsequence descriptor: 2279 * rows[x][0] - query attribute name 2280 * rows[x][1] - query attribute value 2281 * OUTPUT: that.need_user_input - true if user input needed 2282 * if true: 2283 * that.paramdata - a paramdata object containing information about parameters to be entered: 2284 * paramdata is an array of paraminfo objects: 2285 * name - name of the parameter 2286 * type - name of the type of the parameter 2287 * value - optionally already present value 2288 * attributes - optional additional attributes concerning the entry of this parameter 2289 * dtype - source of the request for this parameter: 'param'/'attr'/'ptype' 2290 * index - index in the respective request structure 2291 * that.names_needed - a paramdata object containing only the needed parameters (no value supplied) 2292 * DESCRIPTION: populate <that> query's parameters with data from <result> (possibly read from RW DQUERY/<dataname>) 2293 */ 2294 function prepare_dquery_params(that,result) { 2295 delete that.query.querytype; 2296 delete that.query.sql; 2297 delete that.query.paramnames; 2298 delete that.query.attributenames; 2299 delete that.query.headertext; 2300 delete that.query.info; 2301 delete that.query.desctext; 2302 delete that.query.title; 2303 var pnam, pvar, pnames, bo, pns; 2304 var pdata = new Array(); // pdata collects parameter data with current value to be edited by user if some are missing 2305 // 2306 // phase 1: transfer query attributes from query descriptor to query context (that.query) 2307 // 2308 for (var qi=0; qi<result.rows.length; qi++) { 2309 pnam = result.rows[qi][0]; 2310 pval = result.rows[qi][1]; 2311 if (!pnam) continue; // ignore config items with no name 2312 if (pnam.indexOf(".")>=0) { // is this a multi-level name ? 2313 pnames = pnam.split("."); // yes, split it into its segments 2314 bo = that.query; 2315 pns = ""; 2316 for (var pi=0; pi<pnames.length; pi++) { // traverse the path described by the segments, starting with the query object 2317 // logger.debug("level "+pnames[pi]); 2318 if (pi<pnames.length-1) { 2319 if (!bo.hasOwnProperty(pnames[pi])) { 2320 bo[pnames[pi]] = {}; 2321 // logger.debug("created level "+pnames[pi]); 2322 } 2323 bo = bo[pnames[pi]]; 2324 pns += (pns.length==0?"":".")+pnames[pi]; 2325 } else { 2326 bo[pnames[pi]] = pval; 2327 } 2328 } 2329 } else { 2330 that.query[pnam] = pval; 2331 } 2332 } 2333 // 2334 // phase 2: get required parameters and check if all are present 2335 // required parameters are specified in: 2336 // [paramnames] 2337 // [attributenames] 2338 // [paramtype] 2339 aux.default_param(that.query,'querytype','dbselect'); 2340 var sppn = 'paramnames_'+db.getCurrentConfigDBtype(); 2341 var specparamnames = null; 2342 if (that.query.hasOwnProperty(sppn)) { 2343 specparamnames = that.query[sppn]; // let [paramnames_<db-type>] override [paramnames] 2344 that.query.paramnames = specparamnames; 2345 } 2346 aux.default_param(that.query,'paramnames',""); 2347 aux.default_param(that.query,'attributenames',""); 2348 aux.default_param(that.query,'paramtype',""); 2349 that.query.params = new Array(); // collect query params here 2350 that.need_user_input = false; 2351 that.parameters_needed = new Array(); // collect needed params here 2352 that.names_needed = {}; 2353 var namcomps, curname, curvalue, curtype, curatt; 2354 // 2355 // phase 2.a: gather from [paramnames], these are required for dbselect type queries and bound to bind-variables 2356 // 2357 if (that.query.paramnames.length>0) { // parameters are necessary 2358 that.paramnames = that.query.paramnames.split(","); 2359 for (var pi = 0; pi<that.paramnames.length; pi++) { // check all necessary bind-variable-parameters 2360 curtype = "string"; 2361 curname = that.paramnames[pi]; 2362 curvalue = ''; 2363 if (curname.indexOf(":")>=0) { 2364 namcomps = curname.split(":"); 2365 if (namcomps.length>0) curname = namcomps[0]; 2366 if (namcomps.length>1) curtype = namcomps[1]; // name has a type attached 2367 if (namcomps.length>2) curatt = namcomps[2]; // name has additional attributes attached 2368 } 2369 if (that.query.hasOwnProperty(curname)) { // do we have the parameter already as a query attribute? 2370 curvalue = that.query[curname]; 2371 that.query.params.push(curvalue); // yes, take it from query 2372 } else { // no, not specified in query 2373 if (!(that.names_needed[curname])) { // have we already requested it? 2374 that.parameters_needed.push(curname); // don't have it, signal it 2375 that.names_needed[curname] = {dtype: 'param', type: curtype, attributes: curatt, index: pi}; // memorize it as already requested as parameter 2376 } 2377 that.need_user_input = true; // no, signal that we need user input 2378 } 2379 if (!find_name(pdata,curname)) { 2380 pdata.push({name: curname, type: curtype, value: curvalue, attributes: curatt, dtype: 'param'}); 2381 } 2382 } 2383 } 2384 // 2385 // phase 2.b: gather from [attributenames] 2386 // 2387 that.query.attributes = {}; 2388 if (that.query.attributenames.length>0) { // required attributes from query descriptor 2389 that.attributenames = that.query.attributenames.split(","); 2390 for (var ai = 0; ai<that.attributenames.length; ai++) { // check all required attributes' names 2391 curtype = 'string'; 2392 curname = that.attributenames[ai]; 2393 curvalue = ''; 2394 if (curname.indexOf(":")>=0) { 2395 namcomps = curname.split(":"); 2396 if (namcomps.length>0) curname = namcomps[0]; 2397 if (namcomps.length>1) curtype = namcomps[1]; // name has a type attached 2398 if (namcomps.length>2) curatt = namcomps[2]; // name has additional options attached 2399 } 2400 if (that.query.hasOwnProperty(curname)) { // do we have a query attribute with this name? 2401 curvalue = that.query[curname]; // yes, get its value from the query 2402 that.query.attributes[curname] = curvalue; // use it 2403 } else { // we don't have a query attribute with this name 2404 if (!(that.names_needed[curname])) { // have we already requested it? 2405 that.parameters_needed.push(curname); // don't have it, signal it 2406 that.names_needed[curname] = {dtype: 'attr', type: curtype, attributes: curatt, index: ai}; // memorize as already requested as attribute 2407 } 2408 that.need_user_input = true; 2409 } 2410 if (!find_name(pdata,curname)) { 2411 pdata.push({name: curname, type: curtype, value: curvalue, attributes: curatt, dtype: 'attr'}); 2412 } 2413 } 2414 } 2415 // 2416 // phase 2.c: gather from [paramtype] 2417 // 2418 if (that.query.paramtype) { 2419 curtype = that.query.paramtype; 2420 // TODO: UNDER CONSTRUCTION !!! 2421 curname = 'paramobject' 2422 curvalue = {}; 2423 curatt = ''; 2424 if (curname.indexOf(':')>=0) { 2425 namcomps = curname.split(":"); 2426 if (namcomps.length>0) curname = namcomps[0]; 2427 if (namcomps.length>1) curtype = namcomps[1]; // name has a type attached 2428 if (namcomps.length>2) curatt = namcomps[2]; // name has additional options attached 2429 } 2430 if (that.query.hasOwnProperty(curname)) { // do we have a query attribute 'paramobject' already? 2431 curvalue = that.query[curname]; 2432 that.query.attributes[curname] = curvalue; 2433 } else { // don't have a query attribute with this name 2434 if (!(that.names_needed[curname])) { // do we alread have requested it ? 2435 that.parameters_needed.push(curname); // don't have it, signal it 2436 that.names_needed[curname] = {dtype: 'ptype', type: curtype, attributes: curatt, index: -1}; // memorize as already requested by paramtype 2437 } 2438 that.need_user_input = true; 2439 } 2440 if (!find_name(pdata,curname)) { 2441 pdata.push({name: curname, type: curtype, value: curvalue, attributes: curatt, dtype: 'ptype'}); 2442 } 2443 } 2444 // 2445 // phase 3: now resolve placeholders in value fields and attributes value fields 2446 // 2447 for (var i = 0; i<pdata.length; i++) { 2448 if (pdata[i].value) { 2449 if (typeof pdata[i].value === 'string') { 2450 pdata[i].value = aux.populate(pdata[i].value,that.query,that); 2451 if (pdata[i].value.charAt(0)=='{') { 2452 try { 2453 pdata[i].value = JSON.parse(pdata[i].value); 2454 } 2455 catch (e) { 2456 var errmsg = "Error JSON-parsing param-data value of \""+pdata[i].name+"\": "+e.message 2457 logger.error(errmsg); 2458 pdata[i].value = {error: errmsg, input: pdata[i].value}; 2459 } 2460 } 2461 } 2462 } 2463 if (pdata[i].attributes) { 2464 if (typeof pdata[i].attributes === 'string') { 2465 pdata[i].attributes = aux.populate(pdata[i].attributes,that.query,that); 2466 // logger.debug("PREPARE_DQUERY_PARAMS.3.INDEX="+i+".ATTRIBUTES=\""+pdata[i].attributes+"\""); 2467 if (pdata[i].attributes.charAt(0)=='{') { 2468 try { 2469 pdata[i].attributes = JSON.parse(pdata[i].attributes); 2470 } 2471 catch (e) { 2472 var errmsg = "Error JSON-parsing param-data attributes of \""+pdata[i].name+"\": "+e.message 2473 logger.error(errmsg); 2474 pdata[i].attributes = {error: errmsg, input: pdata[i].attributes}; 2475 } 2476 } 2477 } 2478 } 2479 } 2480 that.paramdata = pdata; 2481 // logger.debug("PDATA:"); 2482 // logger.debug(pdata); 2483 } 2484 2485 /* ***************************************************************************** 2486 * FUNCTION: process_query 2487 * INPUT: that - query execution context 2488 * phase - name of extension hook to be invoked 2489 * resattnam - OPTIONAL: name of attribute of <that> to which the 2490 * result of the expression is stored (if any) 2491 * OUTPUT: that - possible changes by executed expression 2492 * DESCRIPTION: processes query extension hook javascript expression 2493 * it is assumed that <that> has an attribute named <phase> 2494 * this attribute shall contain a javascript-expression 2495 * this expression is executed 2496 * if the expression produces a return value, this value 2497 * is logged but not further used or kept 2498 * before execution (with javascript eval()-function), placeholders 2499 * delimited by {{..}} are replaced (using aux.populate()-function) 2500 * If the expression returns a result and <resattnam> is specified, 2501 * the result is stored to the attribute <resattnam> of <that> 2502 */ 2503 2504 function process_query(that,phase,resattnam) { 2505 var procparname = phase; 2506 var donotcatch = prefs.do_not_catch_in[procparname] || false; 2507 var procexpr = ''; 2508 if (that[procparname]) { 2509 procexpr = aux.populate(''+that[procparname],that); 2510 } else { 2511 if ((that.query) && (that.query[procparname])) { 2512 procexpr = aux.populate(''+that.query[procparname],that); 2513 } else { 2514 procexpr = ''; 2515 // logger.debug('DQUERY: no JS-expression '+procparname+' defined'); 2516 return; 2517 } 2518 } 2519 // logger.debug("PROCESS_QUERY.PROCPAR["+procparname+"(populated)]=\""+procexpr+"\""); 2520 var procres; 2521 if (donotcatch) { 2522 procres = eval(procexpr); 2523 } else { 2524 try { 2525 procres = eval(procexpr); // now really execute the JS-expression 2526 } 2527 catch (e) { 2528 logger.error(e); 2529 var nerr = new Error("Error in PROCESS_QUERY.PROCPAR["+procparname+"(populated)]=\""+procexpr+"\": "+e.message); 2530 logger.error(nerr.message); 2531 procres = {error: nerr}; 2532 } 2533 } 2534 if (procres) { 2535 if (resattnam) { 2536 // logger.debug("PROCESS_QUERY.RESULT_TO_THAT[\""+resattnam+"\"]"); 2537 that[resattnam] = procres; 2538 } 2539 } else { 2540 // logger.debug("PROCESS_QUERY.RESULT["+procparname+"]=undefined"); 2541 } 2542 } 2543 2544 /* ***************************************************************************** 2545 * REPORT CREATION 2546 * a report is created out of an already existing [dbresult] structure 2547 * a report is hierarchical, it can have an arbitrary number of grouping levels 2548 * groups are groups of data records sharing a common data value in the discriminator fields 2549 * the discriminator fields are similar to the arguments to a GROUP BY clause in SQL 2550 * each grouping level has: 2551 * - a discriminator, specifying the data fields to determine the group change from 2552 * - a list of data fields listed at this grouping level 2553 * - a format template for group headers 2554 * - a format template for group footers 2555 * - a format template for a group label 2556 */ 2557 // create the grouping control structure: grpctl is an array with one entry per grouping level: 2558 // levelname - name of the level 2559 // dkey - current reference data key used for group change detection 2560 // nkey - data key value for currently processed input data row 2561 // dicnames- array of names of the columns to be included in group change checks 2562 // dacnames- array of names of the columns to be included in data rows on this level 2563 function rep_init_grpctl(ctx) { 2564 var cgln; 2565 var cgl; 2566 ctx.grpnum = 0; 2567 ctx.grlnum = 0; 2568 ctx.maxgrl = -1; 2569 aux.default_param(ctx.query,"grouplevels",""); // per default, the report is single-level 2570 if (ctx.query.grouplevels.length<1) { 2571 ctx.grplvlnames = [""]; 2572 } else { 2573 ctx.grplvlnames = ctx.query.grouplevels.split(","); 2574 } 2575 ctx.grpctl = new Array(); 2576 for (var gli=0; gli<ctx.grplvlnames.length; gli++) { // group levels start with 0 as the highest/coarsest and upwards growing finer 2577 cgln = ctx.grplvlnames[gli]; 2578 cgl = { level: gli, levelname: cgln}; // level number and level name 2579 aux.default_param(ctx.query,cgln+'repqrydisc',ctx.metaData[0].name); // default for repqrydisc is first column of dbselect result 2580 cgl.dicnames = ctx.query[cgln+'repqrydisc'].split(','); // list of columns to be contained in group change discriminator 2581 aux.default_param(ctx.query,cgln+'datacolumns', // default for datacolumns are all columns except the ones in repqrydisc 2582 function(g) { 2583 var dl = new Array(); 2584 var cmn; 2585 for (var di=0; di<ctx.metaData.length; di++) { 2586 cmn = ctx.metaData[di].name; 2587 if (!(aux.find_in_array(g.dicnames,cmn))) { 2588 dl.push(cmn); 2589 } 2590 } 2591 return dl.join(','); 2592 },cgl); 2593 cgl.dacnames = ctx.query[cgln+'datacolumns'].split(','); // list of datacolumns to be listed in report data 2594 aux.default_param(ctx.query,cgln+'grphead', // default for grphead is "Gruppe: ", followed by list of discriminator field values 2595 function(g) { 2596 var ght = "Gruppe: "; 2597 for (var di=0; di<g.dicnames.length; di++) { 2598 ght += "[["+g.dicnames[di]+"]]"; 2599 if (di<(g.dicnames.length-1)) { 2600 ght += " - "; 2601 } 2602 } 2603 return ght; 2604 },cgl); 2605 cgl.grphead = ctx.query[cgln+'grphead']; 2606 aux.default_param(ctx.query,cgln+'grpfoot',"Ende von "+cgl.grphead); // default for grpfoot is "Ende von Gruppe: ",followed by group header 2607 cgl.grpfoot = ctx.query[cgln+'grpfoot']; 2608 aux.default_param(ctx.query,cgln+'grplabel',"[["+cgl.dicnames[cgl.dicnames.length-1]+"]] ([[DATARECNUM]]) - default"); 2609 cgl.grplabel = ctx.query[cgln+'grplabel']; 2610 aux.default_param(ctx.query,cgln+'grplabeltitle',"[["+cgl.dicnames[cgl.dicnames.length-1]+"]]"); 2611 cgl.grplabeltitle = ctx.query[cgln+'grplabeltitle']; 2612 aux.default_param(ctx.query,cgln+'addparams', 2613 function(gl) { 2614 var gap = "{"; 2615 gap += "\"div_left_margin\":\""+(cgl.level*2)+"em\"" 2616 gap += "}"; 2617 return gap; 2618 },cgl); 2619 try { 2620 cgl.addparams = JSON.parse(ctx.query[cgln+"addparams"]); 2621 } catch (e) { 2622 logger.error(); 2623 cgl.addparams = {status: "ERROR", errortext: "REP_INIT_GRPCTL.ERROR_PARSING_JSON["+cgln+"addparams]=\""+ctx.query[cgln+"addparams"]+"\": "+e.message, error: e}; 2624 logger.error(cgl.addparams.errortext); 2625 } 2626 aux.default_param(ctx.query,cgln+"grprowtitle",null); 2627 cgl.grprowtitle = ctx.query[cgln+"grprowtitle"]; 2628 aux.default_param(ctx.query,cgln+"grpcoltitle",null); 2629 cgl.grprowtitle = ctx.query[cgln+"grpcoltitle"]; 2630 ctx.grpctl.push(cgl); 2631 } 2632 ctx.grlnum = ctx.grpctl.length; // number of grouping levels 2633 ctx.maxgrl = ctx.grpctl.length - 1; // highest (=finest) grouping level 2634 } 2635 2636 // add a sub-groups structure to the group GRP 2637 function rep_addgroups(ctx,grp,options) { 2638 var nocascade = false; 2639 if (options) { 2640 nocascade = options.nocascade || false; 2641 } 2642 if (!(grp)) { 2643 logger.error("REP_ADDGROUPS.GRP_IS_NULL_OR_UNDEF!!"); 2644 return; 2645 } 2646 grp.groups = new Array(); 2647 if (!nocascade) { 2648 rep_grps_addgroup(ctx,grp); 2649 } 2650 } 2651 2652 // create a new group of level LVL and all its sub-groups to cover all grouping levels 2653 function rep_newgroup(ctx,lvl,options) { 2654 var ng = {status: "fresh"}; 2655 ng.ctx = ctx; 2656 ctx.grpnum++; 2657 ng.grpid = ctx.upfx+"grp"+ctx.grpnum; 2658 ng.level = lvl; 2659 // logger.debug("REP_NEWGROUP.START.LEVEL="+ng.level+".GRPID="+ng.grpid); 2660 ng.colvalues = {}; 2661 aux.copyObject(ng.colvalues,ctx.colvalues); 2662 ng.colvalues.end = {}; 2663 aux.copyObject(ng.colvalues.end,ctx.colvalues); 2664 if ((lvl>(ctx.maxgrl+1)) || (lvl<0)) { 2665 logger.error("REP_NEWGROUP.LEVEL="+lvl+".LEVEL_OUT_OF_RANGE!!"); 2666 logger.error((new Error()).stack); 2667 return ng; 2668 } 2669 ng.levelname = (ng.level==(ctx.maxgrl+1))?"data_detail":ctx.grpctl[ng.level].levelname; 2670 ng.grphead = (ng.level==0)?ctx.query.totalhead:ctx.grpctl[ng.level-1].grphead; 2671 ng.grpfoot = (ng.level==0)?ctx.query.totalfoot:ctx.grpctl[ng.level-1].grpfoot; 2672 ng.grplabel = (ng.level==0)?"<NO_LABEL_AT_ROOTGROUP>":ctx.grpctl[ng.level-1].grplabel; 2673 ng.grplabeltitle= (ng.level==0)?"<NO_LABELTITLE_AT_ROOTGROUP>":ctx.grpctl[ng.level-1].grplabeltitle; 2674 ng.head = aux.populate({ template: ng.grphead, 2675 rex: ctx.phrex, 2676 data: ng.colvalues, 2677 adata: ng}); 2678 if (ng.level==(ctx.maxgrl+1)) { 2679 ng.result = {resulttype: "dbresult"}; 2680 ng.result.metaData = new Array(); 2681 for (var ci=0; ci<ctx.grpctl[ctx.maxgrl].dacnames.length; ci++) { 2682 ng.result.metaData.push({name: ctx.grpctl[ctx.maxgrl].dacnames[ci]}); 2683 } 2684 ng.result.rows = new Array(); 2685 } else { 2686 ng.dkey = rep_build_key(ctx,ng.level); 2687 rep_addgroups(ctx,ng,options); 2688 } 2689 aux.copyObject(ng,(ng.level>ctx.maxgrl)?{}:ctx.grpctl[ng.level].addparams); 2690 return ng; 2691 } 2692 2693 // close GRP 2694 // before closing, write terminating group footer and group label 2695 function rep_closegroup(ctx,grp,options) { 2696 var alertclose = false; 2697 if (options) { 2698 alertclose = options.alertclose || false; 2699 } 2700 if (!grp) { 2701 return; 2702 } 2703 if (grp.status=="closed") { // already closed, do nothing 2704 return; 2705 } 2706 if (alertclose) { 2707 logger.error("REP_CLOSEGROUP.CLOSING."+rep_grpinfo(ctx,grp)+".HAS_TO_BE_CLOSED_UNEXPECTEDLY"); 2708 } 2709 grp.datarecnum = rep_grp_cnt_datarecs(ctx,grp); 2710 grp.colvalues["DATARECNUM"] = grp.datarecnum; 2711 grp.colvalues.end = {}; 2712 aux.copyObject(grp.colvalues.end,ctx.colvalues.prev); 2713 if (grp.level>(ctx.maxgrl+1)) { 2714 logger.error("REP_CLOSEGROUP."+rep_grpinfo(ctx,grp)+".LEVEL="+grp.level+".TOO_HIGH"); 2715 grp.foot = "---"; 2716 grp.label = ">>>"; 2717 grp.labeltitle = "Daten"; 2718 } else { 2719 if (grp.grpfoot) 2720 grp.foot = aux.populate({ template: grp.grpfoot, 2721 rex: ctx.phrex, 2722 data: grp.colvalues, 2723 adata: grp}); 2724 if (grp.grplabel) 2725 grp.label = aux.populate({ template: grp.grplabel, 2726 rex: ctx.phrex, 2727 data: grp.colvalues, 2728 adata: grp}); 2729 if (grp.grplabeltitle) 2730 grp.labeltitle = aux.populate({ template: grp.grplabeltitle, 2731 rex: ctx.phrex, 2732 data: grp.colvalues, 2733 adata: grp}); 2734 } 2735 delete grp.ctx; // unlink grp from ctx (don't need it after closing) 2736 grp.status = "closed"; 2737 } 2738 2739 // close GRP and all its sub-groups 2740 function rep_closeallgroups(ctx,grp,options) { 2741 if (!grp) { 2742 return; 2743 } 2744 if (grp.groups) { 2745 for (var i=0; i<grp.groups.length; i++) { 2746 rep_closeallgroups(ctx,grp.groups[i],options); 2747 } 2748 } 2749 rep_closegroup(ctx,grp,options); 2750 } 2751 2752 // add a new subgroup to GRP 2753 function rep_grps_addgroup(ctx,grp) { 2754 if (grp) { 2755 if (!(grp.groups)) { 2756 rep_addgroups(ctx,grp); 2757 } else { 2758 grp.groups.push(rep_newgroup(ctx,grp.level+1)); 2759 } 2760 grp.status = "data"; 2761 return grp.groups[grp.groups.length-1]; 2762 } else { 2763 logger.error("REP_GRPS_ADDGROUP.GRP_IS_NULL_UNDEF!!"); 2764 return null; 2765 } 2766 } 2767 2768 // determine if a group has data (sub-groups or data records). This includes its sub-groups at finer levels 2769 function rep_grp_has_data(ctx,grp) { 2770 if (!grp.groups) { 2771 if (!grp.result) { 2772 return false; 2773 } else { 2774 if ((grp.result.rows) && (grp.result.rows.length>0)) { 2775 return true; 2776 } else { 2777 return false; 2778 } 2779 } 2780 } else { 2781 var i = 0; 2782 while (i<grp.groups.length) { 2783 if (rep_grp_has_data(ctx,grp.groups[i])) { 2784 return true; 2785 } else { 2786 i += 1; 2787 } 2788 } 2789 return false; 2790 } 2791 } 2792 2793 // count all data records in the group GRP and all its descendent groups at finer levels 2794 function rep_grp_cnt_datarecs(ctx,grp) { 2795 if (!grp) { 2796 return 0; 2797 } else { 2798 var cnt = 0; 2799 if (grp.groups) { 2800 for (var i=0; i<grp.groups.length; i++) { 2801 cnt += rep_grp_cnt_datarecs(ctx,grp.groups[i]); 2802 } 2803 } 2804 if ((grp.result) && (grp.result.rows)) { 2805 cnt += grp.result.rows.length; 2806 } 2807 return cnt; 2808 } 2809 } 2810 2811 function rep_grpinfo(ctx,grp) { 2812 if (grp) { 2813 return "{GRPID="+((grp.grpid)?grp.grpid:"<UNKNOWN>")+".LEVEL="+((grp.level)?grp.level:"<UNKNOWN>")+".LEVELNAME="+grp.levelname+".GROUPS["+((grp.groups)?grp.groups.length:"NONE")+"]}"; 2814 } else { 2815 return "<NOT_DEFINED>"; 2816 } 2817 } 2818 2819 // perform a group change in CTX for group GRP 2820 // this is done by closing the current group and opening a new empty one (including all sub-levels) 2821 // a special case is when the current group has no data (subgroups or data records) yet. Then it is re-used to avoid empty groups 2822 function rep_lg_chggroup(ctx,rlg) { 2823 if (!(rlg)) { 2824 logger.error("REP_GL_CHGGROUP.NO_LEVELGROUP!!"); 2825 return; 2826 } 2827 if (!(rlg.groups)) { 2828 logger.warn("REP_GL_CHGGROUP.NO_GROUPS"); 2829 rep_addgroups(ctx,rlg); 2830 rlg.dkey = ctx.nkey; 2831 return rep_grps_addgroup(ctx,rlg.groups,rlg.level+1); 2832 } 2833 if (rlg.groups.length<1) { 2834 logger.warn("REP_GL_CHGGROUP.GROUPS_EMPTY"); 2835 rlg.dkey = ctx.nkey; 2836 return rep_grps_addgroup(ctx,rlg.groups,rlg.level+1); 2837 } 2838 var cg = rlg.groups[rlg.groups.length-1]; 2839 // logger.debug("REP_LG_CHGGROUP.CG="+rep_grpinfo(ctx,cg)); 2840 if (rep_grp_has_data(ctx,cg)) { 2841 rep_closeallgroups(ctx,cg); 2842 cg = rep_grps_addgroup(ctx,rlg); 2843 } 2844 rlg.dkey = ctx.nkey; 2845 return cg; 2846 } 2847 2848 // find the group in CTX data is to be written to 2849 // per definitionem the last group in the finest grouping level 2850 // if <grp> is supplied, find the data group starting from <grp> 2851 // if <grp> is not supplied, start from <ctx>'s root group 2852 function rep_find_data_group(ctx,grp) { 2853 var cdgp = grp || ctx.rootgroup; 2854 // logger.debug("REP_FIND_DATA_GROUP_START.CDGP:\r\n--- BEGIN_CDGP ---\n"+JSON.stringify(cdgp,null,4)+"\r\n--- END_CDGP ---"); 2855 while (true) { 2856 // logger.debug("REP_FIND_DATA_GROUP_BODYSTART.CDGP.GRPID="+cdgp.grpid); 2857 if (!cdgp.groups) { 2858 return cdgp; 2859 } 2860 // logger.debug("REP_FIND_DATA_GROUP_GROUPS_THERE.CDGP.GRPID="+cdgp.grpid+".GROUPS.LENGTH="+cdgp.groups.length); 2861 if (cdgp.groups.length<1) { 2862 // logger.error("REP_FIND_DATAGROUP.CDGP.GRPID="+cdgp.grpid+".GROUPS_IS_EMPTY!!"); 2863 cdgp.groups.push({status: "new"}); 2864 grpnum++; 2865 cdgp.groups[cdgp.groups.length-1].grpid = upfx+"grp"+grpnum; 2866 // logger.debug("REP_FIND_DATA_GROUP.NEW_WORKAROUND_SUBGROUP.GRPID="+cdgp.groups[cdgp.groups.length-1].grpid); 2867 } 2868 cdgp = cdgp.groups[cdgp.groups.length-1]; 2869 // logger.debug("REP_FIND_DATA_GROUP_AFTER_STEP.GRPID="+cdgp.grpid+".GROUPS_THERE="+((cdgp.groups)?"TRUE":"FALSE")); 2870 } 2871 } 2872 2873 // write the current data record in CTX to the current data group 2874 function rep_writedata(ctx) { 2875 var cdgp = rep_find_data_group(ctx); 2876 if (!(cdgp)) { 2877 logger.error("REP_WRITEDATA.DATAGROUP_NOT_FOUND!!"); 2878 return; 2879 } 2880 var newrow = new Array(); 2881 var cgl = ctx.grpctl[ctx.maxgrl]; // data row always conforms to data record specification of finest level 2882 for (var ci=0; ci<cgl.dacnames.length; ci++) { 2883 newrow.push(ctx.colvalues[cgl.dacnames[ci]]); 2884 } 2885 if (cgl.grprowtitle) { 2886 newrow["rowtitle"] = aux.populate({ template: cgl.grprowtitle, 2887 rex: ctx.phrex, 2888 data: ctx.colvalues, 2889 adata: ctx}); 2890 } 2891 cdgp.result.rows.push(newrow); 2892 cdgp.status = "data"; 2893 // logger.debug("REP_WRITEDATA.RI="+ctx.ri+".ROWWRITTEN.DATAGROUP="+rep_grpinfo(ctx,cdgp)+".ROWS["+cdgp.result.rows.length+"]"); 2894 } 2895 2896 function rep_update_colvalues(ctx) { 2897 if (!(ctx.colvalues)) { 2898 ctx.colvalues = {prev: {}}; 2899 } 2900 delete ctx.colvalues['prev']; // remove old previous values 2901 var pprev = {}; 2902 for (var pa in ctx.colvalues) { // update previous values to current (i.e. the previous rows') colvalues 2903 pprev[pa] = ctx.colvalues[pa]; 2904 } 2905 ctx.colvalues = { prev: pprev }; // move previous rows' colvalues to colvalues.prev 2906 for (ci=0; ci<ctx.metaData.length; ci++) { // colvalues holds the current row's column values plus the previous ones in .prev 2907 ctx.colvalues[ctx.metaData[ci].name] = ctx.rows[ctx.ri][ci]; 2908 } 2909 ctx.colvalues["ROWNUM"] = ctx.ri; 2910 if (ctx.query.datarecext) { 2911 ctx.colvalues["RECEXT"] = aux.populate({ template: ctx.query.datarecext, 2912 rex: ctx.phrex, 2913 data: ctx.colvalues, 2914 adata: ctx}); 2915 if (typeof(ctx.colvalues["RECEXT"])==='object') { 2916 for (var oan in ctx.colvalues["RECEXT"]) { 2917 ctx.colvalues[oan] = ctx.colvalues["RECEXT"][oan]; 2918 } 2919 } 2920 } 2921 } 2922 2923 function rep_build_key(ctx,lvl) { 2924 if (lvl>(ctx.grpctl.length-1)) { 2925 logger.error("REP_BUILD_KEY.LVL_TOO_HIGH="+lvl+".MAX="+ctx.maxgrl+"/"+(ctx.grpctl.length-1)); 2926 logger.error((new Error()).stack); 2927 return ""; 2928 } 2929 var key = ""; 2930 var cgl = ctx.grpctl[lvl]; 2931 for (var ci=0; ci<cgl.dicnames.length; ci++) { 2932 key += ctx.colvalues[cgl.dicnames[ci]]; 2933 } 2934 return key; 2935 } 2936 2937 // =============== customization support functions ============================= 2938 2939 function rep_datasum(ctx,grp,colname) { 2940 var dgp = rep_find_data_group(ctx,grp); 2941 var sum = 0; 2942 var cr; 2943 var dci = -1; 2944 // find the column containing the column name <colname> 2945 for (var ci=0; ci<dgp.result.metaData.length; ci++) { 2946 if (dgp.result.metaData[ci].name==colname) { 2947 dci = ci; 2948 break; 2949 } 2950 } 2951 if (dci<0) { // if not found, sum defaults to 0 2952 return 0; 2953 } 2954 for (var dri=0; dri<dgp.result.rows.length; dri++) { 2955 cr = dgp.result.rows[dri]; 2956 if (dci<cr.length) { 2957 sum += cr[dci]; 2958 } 2959 } 2960 return sum; 2961 } 2962 2963 2964 /* ***************************************************************************** 2965 * 2966 * FUNCTION: dbreport_postprocess 2967 * INPUT: that - dquery-context of the current dbreport-query 2968 * that.query - query parameters configuring the behaviour: 2969 * repgrpdisc - comma-separated list of .result-columns to be used as group discriminator 2970 * a change in the values of these columns is detected as group-change 2971 * grphead - a populate()d pattern to be output at the head (before) of each group 2972 * grpfoot - a populate()d pattern to be output at the foot (after) of each group 2973 * totalhead - a populate()d pattern to be output at 2974 * totalfoot - a populate()d pattern to be output at 2975 * datacolumns - comma-separated list of .result-columns to be output in each group, 2976 * this is usually the other .result-columns apart from the group discriminator 2977 * OUTPUT: that.result - result object modified to formatted report 2978 * DESCRIPTION: post-processes a 'dbresult' result object into a 'dbreport' result object 2979 */ 2980 function dbreport_postprocess(that) { 2981 // logger.debug("DBREPORT_POSTPROCESS.DEBUG="+that.query.debug); 2982 var dbg = (that)?(that.result)?(that.query.debug)?(that.query.debug=="true"):false:false:false; 2983 if (dbg) { 2984 aux.logJSON(that.query,"DBREPORT_POSTPROCESS.QUERY"); 2985 aux.logJSON(that.result,"DBREPORT_POSTPROCESS.RESULT"); 2986 } 2987 if (!that.result) { 2988 throw new Error("dbreport_postprocess: query context object does not have a result attribute"); 2989 return; 2990 } 2991 if (!(that.result.resulttype=='dbresult')) { 2992 throw new Error("dbreport_postprocess: input result type shall be 'dbresult' but is \""+that.result.resulttype+"\""); 2993 return; 2994 } 2995 var ctx = that.result; 2996 ctx.query = that.query; // let ctx point to query so that placeholders can refer to it 2997 ctx.prefs = prefs; // let ctx point also to global prefs so that placeholders can refer to it 2998 var oldresult = {}; 2999 for (var oro in that.result) { 3000 oldresult[oro] = that.result[oro]; 3001 } 3002 ctx.oldresult = oldresult; 3003 ctx.phrex = /\[\[([\w\.\(\)\#"':;\?@]*)\]\]/g // placeholders must be surrounded by "[[" and "]]", this regex detects them in aux.populate() 3004 ctx.upfx = aux.uniqueNowID(); // unique prefix for this very report instance, in order to produce unique report element identifiers 3005 rep_init_grpctl(ctx); // create the grouping control structure from query parameters 3006 aux.default_param(ctx.query,'totalhead',"Bericht \""+ctx.query.dataname+"\""); // default for totalhead is "Bericht <dataname>" 3007 aux.default_param(ctx.query,'totalfoot',"Ende von "+ctx.query.totalhead); // default for totalfoot is "Ende von <totalhead>" 3008 // now create the dbreport structure 3009 ctx.resulttype = 'dbreport'; 3010 if (ctx.rows.length<1) { 3011 ctx.head = "Keine Daten im Bericht"; 3012 } else { 3013 ctx.ri = 0; // current data record number 3014 rep_update_colvalues(ctx); // populate colvalues from 1st data row for initializing the tree 3015 ctx.repid = ctx.upfx; 3016 ctx.rootgroup = rep_newgroup(ctx,0); // this creates the starting tree implicitely 3017 // create the report heading using values from the first data row 3018 ctx.head = aux.populate({ template: ctx.query.totalhead, 3019 rex: ctx.phrex, 3020 data: ctx.colvalues, 3021 adata: ctx}); 3022 // now entering report processing proper 3023 for (ctx.ri=0; ctx.ri<that.result.rows.length; ctx.ri++) { // iterate over all rows in the flat input result 3024 ctx.curreprow = ctx.ri; 3025 rep_update_colvalues(ctx); // populate colvalues from teh current data row 3026 // now check if any group level detects group change 3027 ctx.cgp = ctx.rootgroup; // start group change detection at root level 3028 while ((ctx.cgp) && (ctx.cgp.level<=ctx.maxgrl)) { 3029 ctx.nkey = rep_build_key(ctx,ctx.cgp.level); 3030 // logger.debug("DBREPORT_POSTPROCESS.CHKGRPCHG.RI="+ctx.ri+".LVL="+ctx.cgp.level+".LEVELNAME="+ctx.cgp.levelname+".GRPID="+ctx.cgp.grpid+ 3031 // ".DKEY=\""+ctx.cgp.dkey+"\".NKEY=\""+ctx.nkey+"\""); 3032 if (ctx.nkey!=ctx.cgp.dkey) { 3033 // logger.debug("DBREPORT.POSTPROCESS.GRPCHGDETECTED.DKEY=\""+ctx.cgp.dkey+"\".NKEY=\""+ctx.nkey+"\""); 3034 ctx.cgp = rep_lg_chggroup(ctx,ctx.cgp); 3035 } 3036 ctx.cgp = (ctx.cgp.groups)?ctx.cgp.groups[ctx.cgp.groups.length-1]:null; // iterate to next level ( last member of current groups' groups-array) 3037 } 3038 // now write the current data row to the result output of the finest level's result data 3039 rep_writedata(ctx); 3040 // logger.debug("DBREPORT_POSTPROCESS.ROW_COMPLETE="+ri); 3041 } // end for result rows 3042 // have written last row of all, close all groups and the whole report 3043 rep_closeallgroups(ctx,ctx.rootgroup); 3044 // create to report footer, based on the data values of the last row in the input 3045 ctx.colvalues["TOTALDATARECS"] = rep_grp_cnt_datarecs(ctx,ctx.rootgroup); 3046 ctx.foot = aux.populate({ template: ctx.query.totalfoot, 3047 rex: ctx.phrex, 3048 data: ctx.colvalues, 3049 adata: ctx}); 3050 } // end if result has rows 3051 delete ctx.cgp; 3052 delete that.result['rows']; // clean out dbresult remainders 3053 delete that.result['metaData']; 3054 delete that.result['oldresult']; 3055 delete that.result["prefs"]; 3056 delete that.result["query"]; 3057 if (dbg) aux.logJSON(that.result,"DBREPORT_POSTPROCESS.PROCESSED_RESULT"); 3058 } // end of dbreport_postprocess() 3059 3060 3061 /* ***************************************************************************** 3062 * 3063 * FLOW: dquery 3064 * INPUT: SESS - session object for checking authorization credentials 3065 * QUERY - query object 3066 * ATTRIBUTES: 3067 * dataname - name of a query descriptor in RW DQUERIES 3068 * format - a format specifier: 3069 * CSV - tabular data in CSV format 3070 * HTAB - HTML table 3071 * CALLBACK: rfunc - called upon successful or failed completion 3072 * CALLBACKPARAMS: ERR - error Object, UNDEFINED if no error 3073 * RESULT - result object 3074 * ATTRIBUTES: 3075 * disposition - 'view' .. shall be rendered in a view 3076 * - 'download' .. plain data to be downloaded 3077 * view - if disposition='view', this is the name of the view to use 3078 * contentType - the MIME-type to be set in the HTTP response 3079 * data - if present, to be passed to return document if disposition != 'view' 3080 * CALLBACK: pfunc - called upon updated progress status 3081 * CALLBACKPARAMS: PROGSTAT - an arbitrary object with attributes reflecting the progress of the query 3082 * DESCRIPTION: gets query parameters from RW DQUERIES 3083 * the query descriptor needs the following rows in table CCRW: 3084 * - one with RWTYPE='DQUERY' an RWPARENT='QUERIES' and RWNAME=<dataname> 3085 * - one or more with RWTYP='DQUERYPARAM' and RWPARENT=<dataname>: 3086 * RWNAME RWVALUE 3087 * querytype type of the query to be executed: 3088 * 'dbselect' ... perform a SELECT in the database 3089 * 'showview' ... show a web view 3090 * renders query-object in handlebars-view with the name from QUERYPARAM 'view' 3091 * view the name of the view to render the result (or the query in case of 'showview' ) 3092 * 'jsexpr' ..... execute the javascript-expression in QUERYPARAM 'aexpression' or 'qexpression': 3093 * aexpression asynchronous javascript expression to execute, must callback <that> upon completion 3094 * qexpression synchronous javascript expression to execute 3095 * 'REST' ....... perform a REST-query defined by following attributes: 3096 * host host name or IP-address to execute REST query at 3097 * endpoint HTTP endpoint for REST-query 3098 * method HTTP method for REST-query: GET,POST, ... 3099 * data data to send with REST-query 3100 * for method='GET' this is appended to the endpoint (HTTP query-string) 3101 * for all other methods it is sent as POST-headers 3102 * DEFAULT: 'dbselect' 3103 * sql the SQL statement to be executed for the query 3104 * DEFAULT: "SELECT 'no SQL provided to PREPARE_DQUERY_PARAMS' as ERROR from DUAL" 3105 * paramnames comma-separated list of names of query parameters to replace bind variables 3106 * attributenames comma-separated list of names of additional query attributes needed 3107 * title text possibly shown in result ahead of the query result data 3108 * info short descriptive text to be logged with query 3109 * DEFAULT: "No info provided" 3110 * desctext descriptive text possibly shown to user 3111 * DEFAULT: "" 3112 * format format in which the result data shall be presented to user (or caller) 3113 * 'htab' ...... tabular HTML format 3114 * 'csv' ....... comma-separated text string 3115 * fieldsep string separating fields in CSV format 3116 * DEFAULT: ',' (comma) 3117 * linesep string separating lines in CSV format 3118 * DEFAULT: '\r\n' (CR+LF) 3119 * pre_query a javascript expression to be processed before the query is executed 3120 * this expression can rely on following 3121 * <others> query-configuration specific attributes used in placeholder replacement 3122 * executes query in database 3123 * formats data according to FORMAT query parameter 3124 */ 3125 3126 var dquery = flow.define( 3127 // step 1: check authorization 3128 function(sess,query,rfunc,pfunc) { 3129 this.session = sess; 3130 this.callback = rfunc; 3131 this.pcallback = pfunc; 3132 this.query = query; 3133 aux.default_param(this.query,"donotquerydescriptor","NO"); 3134 // logger.debug("DQUERY.QUERY: ",this.query); 3135 logger.debug("DQUERY.BEGIN.DATANAME=\""+this.query.dataname+"\".FORMAT=\""+this.query.format+"\""); 3136 // logger.debug("SESSION:"); 3137 // logger.debug(this.session); 3138 functions.check_authorization(this.session,'dquery',this,this); 3139 }, 3140 // step 1a: get query descriptor data 3141 function(err,result) { 3142 if (err) { 3143 aux.error_callback(this.callback,err,"Authorization error in DQUERY",result); 3144 return; 3145 } 3146 if (this.query.donotquerydescriptor!="YES") { 3147 db.select({ sql: "SELECT RWNAME,RWVALUE,RWDESC FROM CCRW WHERE RWTYP='DQUERYPARAM' and RWPARENT=:DATANAME", 3148 reqid: 'RQID_GETQRYDESC', 3149 info: "Get query descriptor data for DATANAME=\""+this.query.dataname+"\"", 3150 params: [this.query.dataname]},this, 3151 function(progstat) { 3152 // logger.debug("DQUERY.GETQRYDESC.PROGRESS:"); 3153 // logger.debug(progstat); 3154 var pstat = {action: 'dquery', 3155 stage: '1/2 - getquerydesc', 3156 subaction: progstat}; 3157 if (this.pcallback) this.pcallback(pstat); 3158 }.bind(this)); 3159 } else { 3160 this(); 3161 } 3162 }, 3163 // step 2: SELECT for query descriptor completed, check, prepare and execute query 3164 function(err,result) { 3165 if (err) { 3166 aux.error_callback(this.callback,err,"Error in DQUERY/GetQryDesc",result); 3167 return; 3168 } 3169 if (this.query.donotquerydescriptor!="YES") { 3170 if (result.rows.length<1) { 3171 aux.error_callback(this.callback,null,"Error in DQUERY/GetQryDesc: no query descriptor data for dataname=\""+this.query.dataname+"\" - no data returned from database",result); 3172 return; 3173 } 3174 aux.default_param(this.query,'options',{}); 3175 prepare_dquery_params(this,result); // check if all input parameters (as specified in <result>(query descriptor)) are there and get them into this.query 3176 } 3177 if (this.need_user_input) { // cannot execute query, user input needed 3178 this.paramspec = {}; 3179 this.paramspec.resulttype = 'paramspec'; 3180 this.paramspec.dataname = this.query.dataname; 3181 this.paramspec.paramdata = this.paramdata; 3182 var errmsg = "Need input for query "+this.paramspec.dataname+" parameters"; 3183 var err = new Error(errmsg); 3184 aux.error_callback(this.callback,err,errmsg,this.paramspec); 3185 return; 3186 } else { // all parameters there, execute query 3187 process_query(this,'pre_query'); // apply any preprocessing 3188 if (this.query.hasOwnProperty('sqlexpr')) { // "sqlexpr" provides an SQL string built by a javascript expression and overrides "sql" 3189 that = this; 3190 this.query.sql = eval(''+this.query.sqlexpr); 3191 } 3192 if (this.query.hasOwnProperty('sql_'+db.getCurrentConfigDBtype())) { // "sql_<db-type>" provides database-specific SQL and overrides "sql" 3193 this.query.sql = this.query['sql_'+db.getCurrentConfigDBtype()]; 3194 } 3195 aux.populate_all_strings(this.query, this.query.attributes, this.query); 3196 if (this.query.title) this.query.title = aux.populate(this.query.title,this.query.attributes); 3197 this.isSequence = false; 3198 switch (this.query.querytype) { 3199 case 'dbselect': // query type is select in database 3200 case 'dbreport': // query type is report, but the initial step is the same as dbselect 3201 aux.default_param(this.query,'info','DQUERY.'+this.query.querytype+'.DATANAME="'+this.query.dataname+'"'); 3202 aux.default_param(this.query,'title','Datenbank-Abfrage-Ergebnis von '+this.query.dataname+'('+this.query.params.join(',')+')'); 3203 aux.default_param(this.query,'sql',"SELECT 'no SQL provided to DQUERY' as ERROR from DUAL"); 3204 // aux.default_param(this.query,'sql_mysql',"SELECT 'no SQL provided to DQUERY \""+this.query.dataname+"\"' as ERROR"); 3205 aux.default_param(this.query,'paramnames',""); 3206 this.query.dbr = new db.Request({sql: this.query.sql, 3207 // sql_mysql: this.query.sql_mysql, 3208 reqid: 'RQID_SQLQRY_'+this.query.dataname, 3209 info: this.query.info, 3210 headertext: this.query.headertext, 3211 desctext: this.query.desctext, 3212 params: this.query.params, 3213 options: this.query.options}); 3214 // logger.debug("DQUERY.'+this.query.querytype+'.DBR"); 3215 // logger.debug(this.query.dbr); 3216 db.select(this.query.dbr, 3217 this, 3218 function(progstat) { 3219 // logger.debug("DQUERY.DOQUERY.PROGRESS:"); 3220 // logger.debug(progstat); 3221 var pstat = {action: 'dquery', 3222 stage: '2/2 - exec', 3223 subaction: progstat}; 3224 if (this.pcallback) this.pcallback(pstat); 3225 }.bind(this)); 3226 break; 3227 case 'REST': // query type REST is invoking a RESTful Web-Service 3228 aux.default_param(this.query,'info','DQUERY.REST.DATANAME="'+this.query.dataname+'"'); 3229 aux.default_param(this.query,'title','REST-Abfrage-Ergebnis von '+this.query.dataname+'('+this.query.params.join(',')+')'); 3230 if (this.query.hasOwnProperty('dataspecexpression')) { 3231 process_query(this,'dataspecexpression'); 3232 } 3233 aux.default_param(this.query,'data',{}); 3234 aux.default_param(this.query,'host','localhost'); 3235 aux.default_param(this.query,'endpoint','/'); 3236 aux.default_param(this.query,'method','GET'); 3237 if ((typeof this.query.data === "string") && (this.query.data.charAt(0) == "{")) { 3238 try { 3239 var qdata = JSON.parse(this.query.data); 3240 this.query.data = qdata; 3241 } 3242 catch (e) { 3243 logger.error("RESTquery Error parsing JSON data attribute "+e.message); 3244 } 3245 } 3246 RESTquery(this.query.host,this.query.endpoint,this.query.method,this.query.data,this, 3247 function(progstat) { 3248 var pstat = {action: 'dquery', 3249 stage: '2/2 - exec[RESTquery]', 3250 subaction: progstat}; 3251 if (this.pcallback) this.pcallback(pstat); 3252 }.bind(this)); 3253 break; 3254 case 'showview': // query type "showview" is simply showing (possible calculated) parameters in a view 3255 aux.default_param(this.query,'view','plain'); // need a view, default is 'plain' (has only {{body}} placeholder 3256 // now resolve all placeholders in the query parameters 3257 for (att in this.query) { 3258 // logger.debug("SHOWVIEW.PARAM["+att+"]"); 3259 if (typeof this.query[att] === "string") { 3260 this.query[att] = aux.populate(this.query[att],this.query); 3261 } 3262 } 3263 this.query.resulttype = 'object'; 3264 this.callback(undefined,this.query); // delegate view rendering to caller 3265 return; 3266 break; 3267 case 'jsexpr': // query type "jsexpr": execute a JS-expression 3268 aux.default_param(this.query,'qexpression','throw new Error("no qexpression given");'); 3269 if (this.query.aexpression) { // we have an aexpression == "Async Expression" to be processed 3270 process_query(this,'aexpression','ares'); // must lead to completion in step 3 !! Error is signalled via this.ares 3271 if ((this.ares) && (this.ares.error)) { 3272 aux.error_callback(this.callback,this.ares.error,'Error processing AEXPRESSION',this.ares); 3273 return; 3274 } 3275 } else { 3276 process_query(this,'qexpression','qres'); // process the expression 3277 if ((this.qres) && (this.qres.error)) { 3278 aux.error_callback(this.callback,this.qres.error,'Error processing QEXPRESSION',this.qres); 3279 return; 3280 } 3281 aux.default_param(this,'result',{title: 'query expression completed'}); 3282 aux.default_param(this.result,'resulttype','object'); 3283 aux.default_param(this,'go_on',false); // if not set, signal not staying in query 3284 aux.default_param(this,'do_rewind',false); 3285 if (this.do_rewind) { 3286 this.REWIND(); 3287 } 3288 if (this.go_on) { // if expression set GO_ON then it is complete 3289 this(this.err,this.result); // expression has to have set this.err and this.result when signalling GO_ON !! 3290 } 3291 } 3292 break; 3293 case 'sequence': // query type 'sequence': process sequence 3294 aux.default_param(this.query,'sequence','NOSEQUENCE'); 3295 this.isSequence = true; 3296 dsequence(this.session,this.query,this,this.pcallback); 3297 break; 3298 default: 3299 this.callback(new Error('Error in DQUERY/1: illegal query type "'+this.query.querytype),this.query+'"'); 3300 return; 3301 } 3302 } 3303 }, 3304 // step 3: query executed, process result 3305 function(err,result) { 3306 if (err) { 3307 aux.error_callback(this.callback,err,"Error in DQUERY/2",result); 3308 return; 3309 } 3310 this.result = result; 3311 // logger.debug("DQUERY.EXECUTED.RESULT_BEFORE_COPY:"); 3312 // logger.debug(this.result); 3313 switch (this.query.querytype) { // determine default post-processing configuration 3314 case 'dbreport': // for 'dbreport', ... 3315 logger.debug("DQUERY: querytpe=dbreport, set default post_query processing"); 3316 aux.default_param(this.query,'post_query','dbreport_postprocess(that)'); // post-process with dbreport_postprocess() 3317 break; 3318 } 3319 process_query(this,'post_query','postqueryres'); // apply any post-processing 3320 if (this.postqueryres && this.postqueryres.error) { 3321 this.result.postqueryres = this.postqueryres; 3322 aux.error_callback(this.callback,this.postqueryres.error,"Error in executing query post-processing",this.result); 3323 return; 3324 } 3325 aux.default_param(this,'result',{}); 3326 for (var qi in this.query) { // copy everything from query to result in case we need it 3327 if ((qi=='dbr')) continue; // ... except: dbr 3328 this.result[qi] = this.query[qi]; 3329 } 3330 if (!this.result.resulttype) { 3331 aux.error_callback(this.callback,null,"Consistency Error: Query Result has no result type:",result); 3332 return; 3333 } 3334 aux.default_param(this.result,'resulttype','string'); 3335 // logger.debug("DQUERY.RESULT.FORMAT="+this.result.format+".VIEW="+this.result.view+".QUERY:"); 3336 // logger.debug(this.query); 3337 if (!this.result.format) { // determine default result format from resulttype 3338 switch (this.result.resulttype) { 3339 case 'editdata': 3340 this.result.format = 'JSON'; 3341 aux.default_param(this.result.view,'editform'); 3342 break; 3343 case 'object': 3344 this.result.format = 'JSON'; 3345 break; 3346 case 'string': 3347 this.result.format = 'plain'; 3348 break; 3349 case 'dbresult': 3350 this.result.format = 'htab'; 3351 break; 3352 case 'dbreport': 3353 this.result.format = 'hgreport'; 3354 break; 3355 case 'plain': 3356 this.result.format = 'plain'; 3357 break; 3358 default: 3359 logger.warn("Unknown result type \""+this.result.resulttype+"\", defaulting format to 'htab'"); 3360 this.result.format = 'htab'; 3361 } 3362 } 3363 logger.debug("DQUERY.RESULT.FORMAT=\""+this.result.format+"\".RESULTTYPE=\""+this.result.resulttype+"\""); 3364 switch (this.result.format) { 3365 case 'csv': // present query result in CSV format, 1st line is headers, field separator is ";", line separator is CRLF 3366 if (this.result.resulttype!='dbresult') { 3367 aux.error_callback(this.callback,null,"DQUERY-Configuration-Error: Result Format "+this.result.format+" does not match result type "+this.result.resulttype+ 3368 " must be 'dbresult' to produce CSV", 3369 this.result); 3370 return; 3371 } 3372 var qrl = new Array(); // array of result lines already joined 3373 var rhn = new Array(); // array of result header names 3374 aux.default_param(this.result,'linesep','\r\n'); // default for line separator is CRLF 3375 aux.default_param(this.result,'fieldsep',','); // default for field separator is COMMA 3376 aux.default_param(this.result,'headersInFirstLine',true); 3377 if (this.result.headersInFirstLine) { 3378 for (var fi = 0; fi<this.result.metaData.length; fi++) { 3379 rhn.push(this.result.metaData[fi].name); // assemble header line 3380 } 3381 qrl.push(rhn.join(this.result.fieldsep)); // put header name line as first in data (if configured by option "headersInFirstLine") 3382 } 3383 for (var di = 0; di<this.result.rows.length; di++) { 3384 qrl.push(this.result.rows[di].join(this.result.fieldsep)); 3385 } 3386 this.result.data = qrl.join(this.result.linesep); 3387 aux.default_param(this.result,'contentType','text/csv'); 3388 aux.default_param(this.result,'disposition','download'); 3389 break; 3390 case 'htab': // present query result as HTML table, rendered per default with view 'tables' or 'table' 3391 aux.default_param(this.result,'disposition','view'); // if not otherwise specified, disposition is to view the result 3392 if (this.result.resulttype!='dbresult') { 3393 aux.error_callback(this.callback,null,"DQUERY-Configuration-Error: Result Format "+this.result.format+" does not match result type "+this.result.resulttype+ 3394 ", must be 'dbresult' to produce HTML-table", 3395 this.result); 3396 return; 3397 } 3398 if (this.isSequence) { // if we're a sequence ... 3399 // logger.debug("DQUERY.htab.SEQUENCE.query:"); 3400 // logger.debug(this.query); 3401 if (!(this.query.view)) { // ... and view was not set by top level query descriptor ... 3402 this.result.view = null; // ... then undefine a possibly present view from a detail query ... 3403 } // ... in order to re-evaluate the default view in the next step 3404 } 3405 if (!(this.result.view)) { // if this result has no (explicitely defined or defaulted) view ... 3406 // logger.debug("VIEW not set, will default"); // ... then derive a default view from the result: 3407 if ((this.result.dbresults) && (this.result.dbresults.length>1)) { // if there is more than one result in the result ... 3408 this.result.view = 'tables' // ... then use the 'tables' view (presenting all results) 3409 } else { // ... if there is only 1 result ... 3410 this.result.view = 'table'; // ... then use the 'table' view (presenting only the base result) 3411 } 3412 // logger.debug("... default view is "+this.result.view); 3413 } 3414 aux.default_param(this.result,'view','tables'); // anyway, the default view is 'tables', if no other defaulting algorithm succeeded 3415 break; 3416 case 'hhlist': // present query result as hierarchical HTML list 3417 if (this.result.resulttype!='object') { 3418 aux.error_callback(this.callback,null,"DQUERY-Configuration-Error: Result Format "+this.result.format+" does not match result type "+this.result.resulttype+ 3419 ", must be 'object' to produce hierarchical HTML-List", 3420 this.result); 3421 return; 3422 } 3423 // logger.debug("DQUERY.COMPLETE.HHLIST.RESOBJECT:\n"+JSON.stringify(this.result.resultobject,null,2)); 3424 this.result.body = object2hierHtmlList(this.result.resultobject,true); 3425 // logger.debug("DQUERY.COMPLETE.HHLIST.BODY=\""+this.result.body+"\""); 3426 aux.default_param(this.result,'disposition','view'); 3427 aux.default_param(this.result,'view','plain'); 3428 break; 3429 case 'hgreport': 3430 if (this.result.resulttype!='dbreport') { 3431 aux.error_callback(this.callback,null,"DQUERY-Configuration-Error: Result Format "+this.result.format+" does not match result type "+this.result.resulttype+ 3432 ", must be 'dbreport' to produce grouped database report", 3433 this.result); 3434 return; 3435 } 3436 aux.default_param(this.result,'disposition','view'); 3437 aux.default_param(this.result,'view','hgreport'); 3438 break; 3439 case 'JSON': 3440 switch (this.result.resulttype) { 3441 case 'object': 3442 // this.result.body = JSON.stringify(JSON.decycle(this.result.resultobject),null,4).replace(/(?:\r\n|\r|\n)/g,"<br/>"); 3443 this.result.body = "<pre>"+JSON.stringify(JSON.decycle(this.result.resultobject),null,4)+"</pre>"; 3444 aux.default_param(this.result,'disposition','view'); 3445 aux.default_param(this.result,'view','plain'); 3446 break; 3447 case 'editdata': 3448 this.result.form_jsondata = JSON.stringify(JSON.decycle(this.result)); 3449 aux.default_param(this.result,'disposition','view'); 3450 aux.default_param(this.result,'view','editform'); 3451 break; 3452 default: 3453 aux.error_callback(this.callback,null,"DQUERY-Configuration-Error: Result Format "+this.result.format+" does not match result type "+ 3454 this.result.resulttype+", must be 'object' or 'editdata' to produce JSON output",this.result); 3455 return; 3456 } 3457 break; 3458 case 'plain': 3459 if (!this.result.body) { 3460 aux.error_callback(this.callback,null,"DQUERY-result-Error: result of query does not contain a 'body' attribute",this.result); 3461 return; 3462 } 3463 aux.default_param(this.result,'disposition','view'); 3464 aux.default_param(this.result,'view','plain'); 3465 break; 3466 default: 3467 aux.error_callback(this.callback,null,"illegal result format \""+this.result.format+"\"",this.result); 3468 return; 3469 } 3470 logger.debug("DQUERY.COMPLETE.DATANAME=\""+this.query.dataname+"\".FORMAT="+this.result.format+".DISPOSITION=\""+this.result.disposition+"\".VIEW=\""+this.result.view+"\""+ 3471 ".CONTENT_TYPE=\""+this.result.contentType+"\".RESULT_TYPE="+this.result.resulttype+"\""); 3472 // logger.debug(this.result); 3473 this.callback(undefined,this.result); 3474 } 3475 // end of flow dquery 3476 ); 3477 3478 3479 /* ***************************************************************************** 3480 * FLOW: RESTquery 3481 * INPUT: host - IP-Address or DNS-name of host to contact 3482 * endpoint - endpoint address in the host 3483 * method - HTTP-Method to use: 3484 * 'GET' 3485 * 'POST' 3486 * data - an object containing the request data 3487 * rfunc - the completion callback function 3488 * pfunc - optional: the progress status reporting function 3489 * CALLBACK: rfunc - called upon completion 3490 * PARAMETERS: 3491 * err - error object if anything bad happened 3492 * result - a result object 3493 * PFUNC - callback function to be called for progress info 3494 * parameters: 3495 * PROG - a hierarchical object describing the progress of the operation 3496 * DESCRIPTION: performs a REST query: 3497 * query URL string is taken from <query.request> 3498 * result contains the (JSON-)decoded response 3499 * 3500 * TODO: as of 29.10.2015, only GET-requests and only JSON-responses are supported 3501 */ 3502 var RESTquery = flow.define( 3503 // step 1: issue query 3504 function (host,endpoint,method,data,rfunc,pfunc) { 3505 this.host = host; 3506 this.endpoint = endpoint; 3507 this.method = method; 3508 this.data = data; 3509 this.port = 80; 3510 this.proxy = prefs.http_proxy_host || "proxy.sozvers.at"; 3511 this.proxy_port = prefs.http_proxy_port || 8080; 3512 this.callback = rfunc; 3513 this.pcallback = pfunc; 3514 this.starttime=new Date(); 3515 this.headers = {}; 3516 logger.debug("RESTquery.BEGIN.HOST=\""+this.host+"\".ENDPOINT=\""+this.endpoint+"\".METHOD=\""+this.method+"\".DATA:",this.data); 3517 this.dataString = JSON.stringify(data); 3518 if (method == 'GET') { 3519 this.endpoint += '?' + querystring.stringify(this.data); 3520 } else { 3521 this.headers = { 3522 'Content-Type': 'application/json', 3523 'Content-Length': this.dataString.length 3524 }; 3525 } 3526 this.options = { 3527 host: this.host, 3528 port: this.port, 3529 path: this.endpoint, 3530 method: this.method, 3531 headers: this.headers 3532 }; 3533 logger.debug("RESTquery.OPTIONS:",this.options); 3534 this.req = http.request(this.options,this); 3535 logger.debug("RESTquery.SEND.DATA=\""+aux.cutString(this.dataString)+"\""); 3536 this.req.write(this.dataString); 3537 this.req.end(); 3538 }, 3539 // step 2: process response 3540 function(res) { 3541 logger.debug("RESTquery.PROCESS_RESPONSE"); 3542 res.setEncoding('utf-8'); 3543 this.responseString = ''; 3544 3545 res.on('data', function(data) { 3546 logger.debug("RESTquery.PROCESS_RESPONSE.DATA"); 3547 this.responseString += data; 3548 if (this.pcallback) this.pcallback({ action: 'RESTquery', stage: 'getResponseData', 3549 stateunit: 'gotChars', state: this.responseString.length}); 3550 }.bind(this)); 3551 3552 res.on('end', function() { 3553 logger.debug("RESTquery.END.RESPONSE=\""+(this.responseString.length>40?this.responseString.substring(0,40)+"...":this.responseString)+"\""); 3554 if (this.responseString.charAt(0)=='{') { // response is JSON-formatted 3555 try { 3556 this.responseObject = JSON.retrocycle(JSON.parse(this.responseString)); 3557 this.callback(null,{resulttype: 'object', resultobject: this.responseObject}); 3558 } 3559 catch (e) { 3560 this.callback(new Error("Error decoding JSON-response: "+e.message)); 3561 } 3562 } else { // no JSON 3563 this.callback(null,{resulttype: 'string', body: this.responseString}); 3564 } 3565 }.bind(this)); 3566 }); 3567 3568 /* ***************************************************************************** 3569 * 3570 * FUNCTION: copy_dbresult 3571 * INPUT: dbres - result structure from a DBSelect database query. Must have: 3572 * metaData - array with objects, column names in .name attribute 3573 * rows - array (of rows) of arrays (of columns) 3574 * rtitle - title of result display 3575 * rdescription - descriptive text displayed aside the result table 3576 * RETURNS: DBRESULT structure with the contents from <dbres>: 3577 * title 3578 * description 3579 * metaData 3580 * rows 3581 * DESCRIPTION: copies the result of a database query into a dbresults structure to be rendered afterwards 3582 * 3583 * 3584 */ 3585 function copy_dbresult(dbres,rtitle,rdescription) { 3586 var dbr = {}; 3587 dbr.title = rtitle; 3588 dbr.description = rdescription; 3589 dbr.metaData = new Array(); 3590 for (var mdi = 0; mdi<dbres.metaData.length; mdi++) { 3591 dbr.metaData.push({name: dbres.metaData[mdi].name}); 3592 } 3593 dbr.rows = new Array(); 3594 for (tdi = 0;tdi<dbres.rows.length;tdi++) { 3595 dbr.rows[tdi] = new Array(); 3596 for (tdf = 0; tdf<dbres.rows[tdi].length; tdf++) { 3597 dbr.rows[tdi][tdf] = dbres.rows[tdi][tdf]; 3598 } 3599 } 3600 return dbr; 3601 } 3602 3603 3604 3605 /* ***************************************************************************** 3606 * FUNCTION: read_file_line_by_line 3607 * INPUT: path - full path to file in local file system 3608 * options - an object containing options 3609 * CALLBACK: rfunc - called upon completion 3610 * PARAMETERS: 3611 * err - present if an error occured 3612 * filedata - array of strings, one per line in the file 3613 * pfunc - called for progress report 3614 * PARAMETERS: 3615 * action - an action state object describing the state of affairs 3616 */ 3617 var read_file_line_by_line = flow.define( 3618 // step 1: read the file into memory 3619 function(path,options,rfunc,pfunc) { 3620 this.path = path; 3621 this.options = options; 3622 this.callback = rfunc; 3623 this.pcallback = pfunc; 3624 this.delimiter = (this.options.linesep || '\r\n'); 3625 this.encoding = (this.options.encoding || 'utf8'); 3626 this.readflags = (this.options.readflags || 'r'); 3627 this.readoptions = {}; 3628 if (this.encoding) this.readoptions.encoding = this.encoding; 3629 if (this.readflags) this.readoptions.flags = this.readflags; 3630 logger.debug("READ_FILE_LINE_BY_LINE.PATH="+this.path); 3631 fs.readFile(path,this.readoptions,this); 3632 }, 3633 //step 2: split file data into lines 3634 function(err,resobj) { 3635 if (err) { 3636 aux.error_callback(this.callback,err,"Error Reading file "+this.path,resobj); 3637 return; 3638 } 3639 if (!util.isString(resobj)) { 3640 aux.error_callback(this.callback,null,"Data Error in READ_FILE_LINE_BY_LINE: file data is not string, check encoding!",resobj); 3641 return; 3642 } 3643 this.filelines = resobj.split(this.delimiter); 3644 logger.debug("READ_FILE_LINE_BY_LINE("+this.path+").LINES_GOT="+this.filelines.length); 3645 this.callback(null,this.filelines); 3646 } 3647 ); 3648 3649 /* ***************************************************************************** 3650 * FUNCTION: convert_buffer_to_hex_dump_result 3651 * INPUT: buffer - Buffer object containing raw bytes 3652 * hdr - result object to be filled with type 'dbresult' hex dump rows 3653 * rowsize - bytes to be dumped per row, default is 32 3654 * OUTPUT: hdr - DBRESULT object filled with hex dump component strings: 3655 * metaData - row heading texts 3656 * rows - array of arrays 3657 * each row contains data for <rowsize> bytes in the file 3658 * column 0 - offset in file 3659 * column 1 - hex dump of <rowsize> bytes starting from that offset 3660 * column 2 - ASCII characters for the same <rowsize> bytes: non-printables are shown as '.' 3661 */ 3662 function convert_buffer_to_hex_dump_result(buf,hdr,rowsize) { 3663 logger.debug("CONVBUF2HEXDRES.BEGIN.NUM_BYTES="+buf.length); 3664 var aa = 0; 3665 var xs, as, cb, cc; 3666 hdr.resulttype = 'dbresult'; 3667 hdr.rows = new Array(); 3668 hdr.metaData = [{name: 'Offset (Hex)'},{name: 'Hex Dump (Hex Bytes)'},{name: 'ASCII'}]; 3669 rowsize = (rowsize || 32); // default for rowsize is 32 bytes 3670 for (ofs = 0; ofs<buf.length; ofs+=rowsize) { 3671 arow = new Array(); 3672 arow.push(HEX(ofs,16)); // column 0: relative offset in HEX representation, fixed width 16 digits 3673 xs = ''; 3674 as = ''; 3675 for (ci = 0; ci<rowsize; ci++) { 3676 aa = ofs+ci; 3677 if (aa>=buf.length) { 3678 xs += ' '; 3679 as += ' '; 3680 } else { 3681 cb = buf.readUInt8(aa,true); 3682 xs += HEX(cb,2) + ' '; 3683 if ((cb>=32) && (cb<=126)) { // only printable characters 3684 cc = String.fromCharCode(cb); 3685 switch (cc) { // perform HTML escaping 3686 case '>': cc = '>'; break; 3687 case '<': cc = '<'; break; 3688 case '&': cc = '&'; break; 3689 } 3690 as += cc; 3691 } else { 3692 as += '.'; // non-printable characters represented by "." 3693 } 3694 } 3695 } 3696 arow.push(xs); // column 1: bytewise HEX dump 3697 arow.push(as); // column 2: ASCII representation (HTML-escaped) 3698 hdr.rows.push(arow); 3699 } 3700 logger.debug("CONVBUF2HEXDRES.COMPLETE"); 3701 } 3702 3703 /* ***************************************************************************** 3704 * 3705 * FLOW: process_mfile 3706 * INPUT: flowctx - flow context in which the action is running 3707 * sess - session object to check authorization 3708 * mfileid - managed file id to process 3709 * options - object containing options controlling behaviour 3710 * CALLBACK: rfunc - called upon completion 3711 * PARAMETERS: 3712 * err - error object in case of malfunction 3713 * result - a DBresult object with the output of processing 3714 * pfunc - called to report progress 3715 * PARAMETERS: 3716 * action - an action state object describing the state of affairs 3717 * DESCRIPTION: processes the data in managed file <mfileid> 3718 */ 3719 var process_mfile = flow.define( 3720 // step 1: check authorization 3721 function(flowctx,sess,mfileid,options,rfunc,pfunc) { 3722 logger.debug("PROCESS_FILE.MFILEID=\""+mfileid+"\".BEGIN"); 3723 this.flowctx = flowctx; 3724 this.callback = rfunc; 3725 this.pcallback = pfunc; 3726 this.options = options; 3727 this.mfileid = mfileid; 3728 this.session = sess; 3729 functions.check_authorization(this.session,'process_file',this,this); 3730 }, 3731 // step 2: get MFILE data 3732 function(err,result) { 3733 if (err) { 3734 aux.error_callback(this.callback,err,"Authorization Error in PROCESS_MFILE",result); 3735 return; 3736 } 3737 functions.get_mfile_data(this.mfileid,this,this.pcallback); 3738 }, 3739 // step 3: read file 3740 function(err,result) { 3741 if (err) { 3742 aux.error_callback(this.callback,err,"Error getting MFILE-info",result); 3743 return; 3744 } 3745 this.mfileinfo = result; 3746 this.open_flags = (this.options.open_flags || 'r'); 3747 this.read_mode = (this.options.read_mode || 'whole_file'); 3748 logger.debug("PROCESS_MFILE.READ_MODE="+this.read_mode); 3749 if (this.read_mode=='whole_file') { 3750 fs.readFile(this.mfileinfo.mfilepath,this.options,this); 3751 } else if (this.read_mode='line_by_line') { 3752 read_file_line_by_line(this.mfileinfo.mfilepath,this,this,this.pcallback); 3753 } else { 3754 this.read_status = 'step_03'; 3755 process_query(flowctx,'start_read'); 3756 this(); 3757 } 3758 }, 3759 // step 4: process file 3760 function(err,resobj) { 3761 if (err) { 3762 aux.error_callback(this.callback,err,"Error processing file step 3"+this.mfileinfo.mfilepath); 3763 return; 3764 } 3765 if (this.read_mode=='whole_file') { // file completely read in one string <resobj> 3766 this.result = { resulttype: 'dbresult', 3767 title: "Inhalt von "+this.mfileinfo.mfilepath, 3768 metaData: [{name:'filedata'}], 3769 rows: [[resobj]]} 3770 if (this.options.process_result) { 3771 this.flowctx.exectx = this; 3772 this.filedata = resobj; 3773 logger.debug("PROCESS_MFILE: process_result is: \""+this.flowctx.query[this.options.process_result]+"\""); 3774 process_query(this.flowctx,this.options.process_result); 3775 } 3776 this.callback(null,this.result); 3777 } else if (this.read_mode=='line_by_line') { // file read complete, <resobj> is an array of strings, representing lines 3778 this.result = {resulttype: 'dbresult', 3779 title: "Inhalt von "+this.mfileinfo.mfilepath, 3780 metaData: [{name: 'linenumber'},{name: 'linedata'}]}; 3781 this.result.rows = new Array(); 3782 for (var i = 0; i<resobj.length; i++) { 3783 this.result.rows.push([i+1,resobj[i]]); 3784 } 3785 this.callback(null,this.result); 3786 } else { 3787 process_query(this.flowctx,'next_read'); 3788 if (this.read_status!='complete') { 3789 this.REWIND(); 3790 this(); 3791 } else { 3792 this.callback(null,this); 3793 } 3794 } 3795 } 3796 ); 3797 3798 /* 3799 * ***************************************************************************************************** 3800 * Utilities 3801 * ***************************************************************************************************** 3802 */ 3803 3804 /* **************************************************************************** 3805 * 3806 * Decoration - formatting etc. for query results 3807 * 3808 * decoration modifies the result object of a query before showing it to the user 3809 * this can be used for formatting (bold text, ...) 3810 * but also for adding hyperlinks 3811 * 3812 * decoration is controlled by query-attributes. 3813 * Depending on the result type, varying query attributes are checked and, 3814 * if present, applied. 3815 * Applying means that the respective data field is replaced by the result of 3816 * placeholder-resolution by using aux.populate() 3817 */ 3818 3819 // helper functions for decorate() 3820 3821 // decorate a dbresult 3822 // INPUT: dbres - the dbresult object to be decorated 3823 // result - the result object being the complete query result and carrying the query attributes 3824 // ctxobj - the query context where other parameters can be taken from 3825 // decoration query attributes: 3826 // decoration.decorate.metadata.<column>, where <column> is the column number, starting from 0 3827 // decoration.decorate_colcell.<column>, where <column is the column number, starting from 0 3828 // available placeholder names: 3829 // placeholder names are resolved in the context of <dbres>, the dbresult object to be decorated. If not found in <dbres>, fallback goes to <ctxobj> 3830 // colnames - an array of the names of the columns, copied from metaData, index starting from 0 3831 // <colname> - the content of the current row's column named <colname> (as taken from metaData) 3832 // 3833 function decorate_dbresult(dbres,result,ctxobj,options) { 3834 var dbg = (options)?((options.dbg)?options.dbg:false):false; 3835 if (dbg) logger.debug("DECORATE_DBRESULT"); 3836 dbres.base_result = result; // place a link to the base result into dbres so that populate() can use it 3837 dbres.colnames = new Array(); 3838 for (var mi=0; mi<dbres.metaData.length; mi++) { 3839 dbres.colnames.push(dbres.metaData[mi].name); 3840 } 3841 if (result.decoration.decorate_metadata) { 3842 for (var mi=0; mi<dbres.metaData.length; mi++) { 3843 if (result.decoration.decorate_metadata.hasOwnProperty(mi)) { 3844 dbres.colname = dbres.metaData[mi].name; 3845 dbres.metaData[mi].name = aux.populate(result.decoration.decorate_metadata[mi],dbres,ctxobj); 3846 } 3847 } 3848 } 3849 if (result.decoration.decorate_colcell) { 3850 var ci, ri, tpl; 3851 for (ri=0; ri<dbres.rows.length; ri++) { 3852 for (ci=0; ci<dbres.metaData.length; ci++) { 3853 dbres[dbres.colnames[ci]] = dbres.rows[ri][ci]; 3854 if (dbg) logger.debug("DECORATE_DBRESULT.SET_COLVALS."+dbres.colnames[ci]+"="+dbres[dbres.colnames[ci]]); 3855 } 3856 for (ci=0; ci<dbres.metaData.length; ci++) { 3857 if (result.decoration.decorate_colcell.hasOwnProperty(ci)) { 3858 tpl = result.decoration.decorate_colcell[ci]; 3859 if (ci<dbres.rows[ri].length) { 3860 dbres.celldata = dbres.rows[ri][ci]; // provide "celldata" for populate() 3861 dbres.rows[ri][ci] = aux.populate(tpl,dbres,ctxobj); 3862 } else { 3863 logger.error("DECORATE_DBRESULT.CANNOT_DECORATE.RI="+ri+".CI="+ci+".CI>LEN="+dbres.rows[ri].length); 3864 } 3865 } 3866 } 3867 } 3868 } 3869 if (result.decoration.decorate_dbresult) { 3870 var rdt = result.decoration.decorate_dbresult; 3871 var tatt; 3872 if (rdt.target) { 3873 tatt = rdt.target; 3874 } else { 3875 tatt = "decoration_target"; 3876 } 3877 if (rdt.expression) { 3878 if (dbg) aux.logJSON(rdt,"DECORATE_DBRESULT"); 3879 dbres[tatt] = aux.populate({template: rdt.expression, data: dbres, adata: ctxobj, dbg: dbg}); 3880 if (dbg) aux.logJSON(dbres[tatt],"DECORATE_DBRESULT."+tatt); 3881 } 3882 } 3883 } 3884 3885 // decorate a report group 3886 // decoration attributes: 3887 // decoration.decorate_dbreport - marker attribute, if this is present and not null, each data group (being a dbresult) 3888 // is decorated as a dbresult using the decoration attributes as in dbresult: 3889 // decoration.decorate_metadata.<column>, where <column> is the column number, starting from 0 3890 // decoration.decorate_colcell.<column>, where <column is the column number, starting from 0 3891 // ATTENTION: this decoration function is only invoked if the query attribute decoration.decorate_dbreport is defined 3892 function decorate_repgroup(grp,result,ctxobj,options) { 3893 var dbg = (options)?((options.dbg)?options.dbg:false):false; 3894 if (dbg) logger.debug("DECORATE_REPGROUP.GRPID="+grp.grpid); 3895 if (grp.result) { 3896 decorate_dbresult(grp.result,result,ctxobj,options); 3897 } 3898 if (grp.groups) { 3899 for (var gi=0; gi<grp.groups.length; gi++) { 3900 decorate_repgroup(grp.groups[gi],result,ctxobj,options); 3901 } 3902 } 3903 } 3904 3905 3906 // decorate a dbreport 3907 // decoration query attributes: 3908 // decoration.decorate_dbreport - marker attribute, if this is present and not null, each data group (being a dbresult) 3909 // is decorated as a dbresult using the decoration attributes as in dbresult: 3910 // decoration.decorate_metadata.<column>, where <column> is the column number, starting from 0 3911 // decoration.decorate_colcell.<column>, where <column is the column number, starting from 0 3912 // available placeholder names: 3913 // placeholder names are resolved in the context of <dbres>, the dbresult object to be decorated 3914 // colnames - an array of the names of the columns, copied from metaData, index starting from 0 3915 // <colname> - the content of the current row's column named <colname> (as taken from metaData) 3916 // 3917 function decorate_dbreport(result,ctxobj,options) { 3918 var dbg = (options)?((options.dbg)?options.dbg:false):false; 3919 var rd = result.decoration.decorate_dbreport; 3920 if (dbg) logger.debug("DECORATE_DBREPORT."+rd); 3921 if (rd && result.rootgroup) { 3922 decorate_repgroup(result.rootgroup,result,ctxobj,options); 3923 } 3924 } 3925 3926 /* **************************************************************************** 3927 * FUNCTION: decorate 3928 * INPUT: result - a RESULT object to be decorated 3929 * result.decoration - a decoration specifier object: 3930 * .decorate_metadata - decoration specifier for metadata / column headers: 3931 * .<number> - decoration for column header of column <number> 3932 * .decorate_colcell - decoration specifier for data cells, by columns: 3933 * .<number> - decoration template for all data cells in column <number> 3934 * .decorate_dpreport - decoration for a 'dbreport' result 3935 * ctxobj - a context object for additional decoration information to be taken from 3936 * OUTPUT: result - decorated 3937 * DESCRIPTION: the contents of <result> are decorated controlled by the result.decoration specification 3938 * decoration is defined to be a visual enhancement of the data in result 3939 * result.decoration is expected to have attributes that specify how the contents of the metaData and rows objects 3940 * shall be modified to become decorated 3941 * sub-objects of result.decoration may be: 3942 * - decorate_metadata[colnum] - a template applied to the metadata object for column colnum 3943 * - decorate_colcell[colnum] - a template applied to each cell in column data in column colnum 3944 */ 3945 function decorate(result,ctxobj) { 3946 if (!(result.decoration)) return; 3947 var dbg = false; 3948 if (result.decoration.debug) dbg = true; 3949 if (dbg) aux.logJSON(result.decoration,"DECORATE.DECORATION"); 3950 result.context = ctxobj; 3951 if ((result.decoration.decorate_dbreport) && (result.resulttype=='dbreport')) { 3952 decorate_dbreport(result,ctxobj,{dbg: dbg}); 3953 } 3954 if ((result.resulttype=='dbresult') && ((result.decoration.decorate_colcell) || (result.decoration.decorate_metadata) || (result.decoration.decorate_dbresult))) { 3955 decorate_dbresult(result,result,ctxobj,{dbg: dbg}); 3956 } 3957 } 3958 3959 /* **************************************************************************** 3960 * 3961 * FUNCTION: object2hierHtmlList 3962 * INPUT: obj - object to be converted 3963 * dd - if true, decycle the object first 3964 * RESULT: HTML text containing a unordered list enumerating the object <obj>s' attributes 3965 * if an attribute is an object itself, a sub-list is recursively created 3966 */ 3967 function object2hierHtmlList(obj, dd) { 3968 // logger.debug("OBJ2HHL.BEGIN"); 3969 var ht = ""; 3970 var o; 3971 if (dd) { 3972 o = JSON.decycle(obj) 3973 } else { 3974 o = obj; 3975 } 3976 // logger.debug("OBJ2HHL.DECYCLED"); 3977 var an; 3978 var at; 3979 at = (typeof o); 3980 logger.debug("OBJ2HHL.AT=\""+at+"\""); 3981 switch (at) { 3982 case 'object': 3983 ht += '<ul>\n' 3984 for (an in o) { 3985 logger.debug("OBJ2HHL.AN=\""+an+"\""); 3986 ht += '<li>'+an+': '+object2hierHtmlList(o[an],false)+'</li>\n'; 3987 } 3988 ht += '</ul>\n' 3989 break; 3990 case 'function': 3991 ht += '[function]'; 3992 break; 3993 case 'number': 3994 case 'boolean': 3995 ht += o.toString(); 3996 break; 3997 case 'string': 3998 ht += o; 3999 break; 4000 default: 4001 ht += '['+at+']'; 4002 } 4003 return ht; 4004 } 4005 4006 /* **************************************************************************** 4007 * FUNCTION: object2nvlist 4008 * INPUT: o - object reference 4009 * options - additional object to add additional attributes into NVlist 4010 * RESULT: NVlist object derived from <o> 4011 * DESCRIPTION: each attribute of <o> is added into the result in the form: 4012 * { name: <attribute-name>, value: "<attribute-value" } 4013 * additionally, all attributes of <options> are copied to the result 4014 */ 4015 function object2nvlist(o,options) { 4016 var res = {}; 4017 var i = 1; 4018 res.nvlist = new Array(); 4019 for (a in o) { 4020 res.nvlist.push({name: a, value: o[a]}); 4021 } 4022 for (a in options) { 4023 res[a] = options[a]; 4024 } 4025 return res; 4026 } 4027 4028 /* **************************************************************************** 4029 * FUNCTION: DEC 4030 * DESCRIPTION: format number as decimal string 4031 * string is padded with leading zeroes to <ndig> places 4032 * if numeric string would be longer than <ndig> the string is 4033 * cut down right-aligned to <ndig> places, higher order places 4034 * to the left are cut off 4035 */ 4036 function DEC(num,ndig) { 4037 var s = num.toString(); 4038 while (s.length<ndig) { 4039 s = '0'+s; 4040 } 4041 if (s.length>ndig) { 4042 s = s.slice(0-ndig); 4043 } 4044 return s; 4045 } 4046 4047 /* **************************************************************************** 4048 * FUNCTION: HEX 4049 * INPUT: num - number to be formatted 4050 * ndig - number of digits to be produced 4051 * DESCRIPTION: format number as hexadecimal string 4052 * string is padded with leading zeroes to <ndig> places 4053 * if numeric string would be longer than <ndig> the string is 4054 * cut down right-aligned to <ndig> places, higher order places 4055 * to the left are cut off 4056 */ 4057 function HEX(num,ndig) { 4058 var s = num.toString(16); 4059 while (s.length<ndig) { 4060 s = '0'+s; 4061 } 4062 if (s.length>ndig) { 4063 s = s.slice(0-ndig); 4064 } 4065 return s; 4066 } 4067 4068 /* **************************************************************************** 4069 * FUNCTION: init_shutdown 4070 */ 4071 function init_shutdown(mode) { 4072 if (!prefs.init_shutdown) { 4073 prefs.init_shutdown = true; 4074 } 4075 logger.debug("INIT_SHUTDOWN, shutdown with mode \""+mode+"\" in "+prefs.restart_timeout_ms+" ms"); 4076 prefs.shutdown_mode = mode; 4077 setTimeout(function() { 4078 do_exit(mode); 4079 }, 4080 prefs.restart_timeout_ms); 4081 logger.debug("CCDB: shutdown/"+mode+" scheduled in "+prefs.restart_timeout_ms+" ms"); 4082 } 4083 4084 /* **************************************************************************** 4085 * FUNCTION: do_exit 4086 */ 4087 function do_exit(mode) { 4088 var ecod; 4089 switch (mode) { 4090 case "restart": 4091 ecod = prefs.restart_exit_code; 4092 break; 4093 case "halt": 4094 ecod = prefs.halt_exit_code; 4095 break; 4096 case "shutdown": 4097 ecod = prefs.shutdown_exit_code; 4098 break; 4099 default: 4100 ecod = prefs.halt_exit_code; 4101 } 4102 logger.debug("DO_EXIT.MODE=\""+mode+"\".EXIT_CODE="+ecod); 4103 setTimeout(function() { 4104 logger.debug("CCDB: will terminate with exit code "+ecod+" with mode "+mode); 4105 process.exit(ecod); 4106 }, 4107 2000); 4108 logger.debug("CCDB: shutdown/"+mode+" with exit code "+ecod+" scheduled in "+2000+" ms"); 4109 } 4110 4111 /* **************************************************************************** 4112 * FUNCTION: login_register_session 4113 */ 4114 function login_register_session(sess) { 4115 if (!(prefs.session_management)) { 4116 prefs.session_management = { actual_session_count: 0 }; 4117 } 4118 if (!(prefs.session_management.actual_session_count)) { 4119 prefs.session_management.actual_session_count = 0; 4120 } 4121 prefs.session_management.actual_session_count++; 4122 logger.debug("REGISTER_SESSION: now "+prefs.session_management.actual_session_count+" sessions registered."); 4123 } 4124 4125 /* **************************************************************************** 4126 * FUNCTION: 4127 */ 4128 4129 function logout_deregister_session(sess) { 4130 if (!(prefs.session_management)) { 4131 prefs.session_management = { actual_session_count: 0 }; 4132 } 4133 if (prefs.session_management.actual_session_count>0) { 4134 prefs.session_management.actual_session_count--; 4135 } 4136 logger.debug("DEREGISTER_SESSION: now "+prefs.session_management.actual_session_count+" sessions registered."); 4137 if (prefs.session_management.actual_session_count==0 && prefs.init_shutdown) { 4138 do_exit(prefs.shutdown_mode); 4139 } 4140 } 4141 4142 /* **************************************************************************** 4143 * FUNCTION: processSessionAction 4144 * INPUT: sess - session in which's context the action shall be performed 4145 * this is needed for credentials 4146 * action - action to be performed 4147 * necessary fields in <action>: 4148 * cmd - a string identifying the action to be performed: 4149 * "dquery" : perform a query 4150 * query - a [query] object passed to the execution function 4151 * DESCRIPTION: performs the action and logs the result 4152 */ 4153 4154 function processSessionAction(sess,action) { 4155 var cmd = action.cmd; 4156 var query = action.query; 4157 logger.debug("PROCSESSACT.CMD=\""+cmd+"\""); 4158 switch (cmd) { 4159 case "dquery": 4160 dquery(sess,query, 4161 function(err,result) { 4162 if (err) logger.error("SESSACTCOMPLERR.Error completing session action: ",err); 4163 if (result) { 4164 logger.debug("SESSACTCOMPLRES.Session Action completion result: "); 4165 switch (result.resulttype) { 4166 case 'string': 4167 logger.debug("-- "+result.body); 4168 break; 4169 case 'dbresult': 4170 if (result.metaData) { 4171 var ls = ''; 4172 for (var i = 0; i<result.metaData.length; i++) { 4173 if (i>0) ls += ' | '; 4174 ls += aux.fixString(result.metaData[i].name,40); 4175 } 4176 logger.debug(ls); 4177 ls = ''; 4178 for (var i = 0; i<result.metaData.length; i++) { 4179 if (i>0) ls += '-+-'; 4180 ls += aux.fixString('-',40,'-'); 4181 } 4182 logger.debug(ls); 4183 } 4184 if (result.rows) { 4185 ls = ''; 4186 for (var r = 0; r<result.rows.length; r++) { 4187 for (var i = 0; i<result.rows[r].length; i++) { 4188 if (i>0) ls += ' | '; 4189 ls += aux.fixString(result.rows[r][i],40); 4190 } 4191 logger.debug(ls); 4192 } 4193 } 4194 // fall through to log other attributes, too 4195 default: 4196 logger.debug("session action result type is \""+result.resulttype+"\", attributes are:"); 4197 for (var a in result) { 4198 logger.debug(aux.fixString(a,32)+': '+aux.cutString(result[a],40)); 4199 } 4200 logger.debug("----------- end of session action result --------------------"); 4201 } 4202 } else { 4203 logger.debug("SESSACTCOMPL, session action "+cmd+" complete"); 4204 } 4205 }, 4206 function(pstat) { 4207 // logger.debug("SESSACTPROG, session action progress: ",pstat); 4208 }); 4209 break; 4210 default: 4211 logger.error("Undefined session action command \""+cmd+"\""); 4212 } 4213 } 4214 4215 /* **************************************************************************** 4216 * FUNCTION: adminSessionTick 4217 * DESCRIPTION: this function is called periodically every second 4218 * is processes the next action in the sessions queue 4219 */ 4220 function adminSessionTick(asess) { 4221 if (asess.busy) { 4222 return; 4223 } 4224 // logger.debug("ADMINSESSIONTICK.ASESS: ",asess); 4225 asess.busy = true; 4226 var entact = asess.queue.get(); 4227 if (entact) { 4228 processSessionAction(asess,entact); 4229 } else { 4230 asess.busy = false; 4231 } 4232 } 4233 4234 /* **************************************************************************** 4235 * FUNCTION: startAdminSession 4236 * DESCRIPTION: starts the ADMIN session 4237 * creates interval timer to 4238 */ 4239 function startAdminSession() { 4240 var asq = aux.createQueue("ADMIN"); 4241 var asess = {queue: asq}; 4242 aux.createGlobalNamedObject("ADMIN_SESSION",asess); 4243 aux.copyObject(asess,prefs.adminsession); 4244 asess.tickTimeout = setInterval(adminSessionTick,1000,asess); 4245 asess.busy = false; 4246 } 4247 4248 4249 /* *********************************************************************************************** 4250 * MAIN PROGRAM 4251 * ***********************************************************************************************/ 4252 4253 hostname = '0.0.0.0'; // listen on all interfaces 4254 logger.debug('Current directory: ' + process.cwd()); 4255 4256 /* 4257 startup_db = process.argv[2]; 4258 if (!startup_db) startup_db = ''; 4259 logger.debug(db.setConfig(startup_db)); 4260 */ 4261 4262 var prefs_initializer = dbConfig.prefs_initializer; 4263 4264 if (prefs_initializer) { 4265 aux.copyObject(prefs,prefs_initializer); 4266 // aux.logPretty(prefs,"PREFS"); 4267 } 4268 4269 prefs.modulerefs = prefs.modulerefs || {}; 4270 4271 aux.createGlobalNamedObject("PREFS",prefs); 4272 4273 logger.debug("PROCESS.ARGV: ",process.argv); 4274 var startup_db = process.argv[2] || os.hostname(); 4275 var startup_action_desc = process.argv[3]; 4276 if (!startup_db) startup_db = ''; 4277 logger.debug(db.setConfig(startup_db)); 4278 4279 var ssl_options = { 4280 key: fs.readFileSync(__dirname + '/ssl/ccdb.pem'), 4281 cert: fs.readFileSync(__dirname + '/ssl/ccdb.crt'), 4282 }; 4283 4284 prefs.server = https.createServer(ssl_options, app); 4285 prefs.server.listen(app.get('port'), hostname, 4286 function(){ 4287 logger.debug('CCDB.Express.JS_in_node.JS.STARTED_HTTPS.HOSTNAME='+hostname+'.ENV='+app.get('env')+'.PORT='+app.get('port')+'.PRESS_CTRL-C_TO_TERMINATE' ); 4288 }); 4289 // 4290 // create the background ADMIN-Session and issue an optional startup action to be executed by the ADMIN-Session 4291 // 4292 4293 startAdminSession(); 4294 if (startup_action_desc) { 4295 logger.debug("STARTUP_ACTION: ",startup_action_desc); 4296 functions.doInAdminSession(new functions.Action(startup_action_desc)); 4297 } 4298 4299 logger.debug("CCDB.END_MAIN")