Include the view when building share_users.
[xtuple] / node-datasource / main.js
1 #!/usr/bin/env node
2
3 /*jshint node:true, indent:2, curly:false, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true,
4 regexp:true, undef:true, strict:true, trailing:true, white:true */
5 /*global X:true, Backbone:true, _:true, XM:true, XT:true, SYS:true, jsonpatch:true*/
6 process.chdir(__dirname);
7
8 Backbone = require("backbone");
9 _ = require("underscore");
10 jsonpatch = require("json-patch");
11 SYS = {};
12 XT = { };
13 var express = require('express');
14 var app;
15
16 (function () {
17   "use strict";
18
19   var options = require("./lib/options"),
20     authorizeNet,
21     schemaSessionOptions = {},
22     privSessionOptions = {};
23
24   /**
25    * Include the X framework.
26    */
27   require("./xt");
28
29   // Loop through files and load the dependencies.
30   // Apes the enyo package process
31   // TODO: it would be nice to use a more standardized way
32   // of loading our libraries (tools and backbone-x) here
33   // in node.
34   X.relativeDependsPath = "";
35   X.depends = function () {
36     var dir = X.relativeDependsPath,
37       files = X.$A(arguments),
38       pathBeforeRecursion;
39
40     _.each(files, function (file) {
41       if (X.fs.statSync(X.path.join(dir, file)).isDirectory()) {
42         pathBeforeRecursion = X.relativeDependsPath;
43         X.relativeDependsPath = X.path.join(dir, file);
44         X.depends("package.js");
45         X.relativeDependsPath = pathBeforeRecursion;
46       } else {
47         require(X.path.join(dir, file));
48       }
49     });
50   };
51
52
53   // Load other xTuple libraries using X.depends above.
54   require("backbone-relational");
55   X.relativeDependsPath = X.path.join(X.basePath, "../lib/tools/source");
56   require("../lib/tools");
57   X.relativeDependsPath = X.path.join(X.basePath, "../lib/backbone-x/source");
58   require("../lib/backbone-x");
59   Backbone.XM = XM;
60
61   // Argh!!! Hack because `XT` has it's own string format function that
62   // is incompatible with `X`....
63   String.prototype.f = function () {
64     return X.String.format.apply(this, arguments);
65   };
66
67   // Another hack: quiet the logs here.
68   XT.log = function () {};
69
70   // Set the options.
71   X.setup(options);
72
73   // load some more required files
74   var datasource = require("./lib/ext/datasource");
75   require("./lib/ext/models");
76   require("./lib/ext/smtp_transport");
77
78   datasource.setupPgListeners(X.options.datasource.databases, {
79     email: X.smtpTransport.sendMail
80   });
81
82   // load the encryption key, or create it if it doesn't exist
83   // it should created just once, the very first time the datasoruce starts
84   var encryptionKeyFilename = X.options.datasource.encryptionKeyFile || './lib/private/encryption_key.txt';
85   X.fs.exists(encryptionKeyFilename, function (exists) {
86     if (exists) {
87       X.options.encryptionKey = X.fs.readFileSync(encryptionKeyFilename, "utf8");
88     } else {
89       X.options.encryptionKey = Math.random().toString(36).slice(2);
90       X.fs.writeFile(encryptionKeyFilename, X.options.encryptionKey);
91     }
92   });
93
94
95   XT.session = Object.create(XT.Session);
96   XT.session.schemas.SYS = false;
97
98   var getExtensionDir = function (extension) {
99     var dirMap = {
100       "/private-extensions": X.path.join(__dirname, "../..", extension.location, "source", extension.name),
101       "/xtuple-extensions": X.path.join(__dirname, "../..", extension.location, "source", extension.name),
102       "/core-extensions": X.path.join(__dirname, "../enyo-client/extensions/source", extension.name),
103       "npm": X.path.join(__dirname, "../node_modules", extension.name)
104     };
105     return dirMap[extension.location];
106   };
107   var useClientDir = X.useClientDir = function (path, dir) {
108     path = path.indexOf("npm") === 0 ? "/" + path : path;
109     _.each(X.options.datasource.databases, function (orgValue, orgKey, orgList) {
110       app.use("/" + orgValue + path, express.static(dir, { maxAge: 86400000 }));
111     });
112   };
113   var loadExtensionClientside = function (extension) {
114     var extensionLocation = extension.location === "npm" ? extension.location : extension.location + "/source";
115     useClientDir(extensionLocation + "/" + extension.name + "/client", X.path.join(getExtensionDir(extension), "client"));
116   };
117   var loadExtensionServerside = function (extension) {
118     var packagePath = X.path.join(getExtensionDir(extension), "package.json");
119     var packageJson = X.fs.existsSync(packagePath) ? require(packagePath) : undefined;
120     var manifestPath = X.path.join(getExtensionDir(extension), "database/source/manifest.js");
121     var manifest = X.fs.existsSync(manifestPath) ? JSON.parse(X.fs.readFileSync(manifestPath)) : {};
122     var version = packageJson ? packageJson.version : manifest.version;
123     X.versions[extension.name] = version || "none"; // XXX the "none" is temporary until we have core extensions in npm
124
125     // TODO: be able to define routes in package.json
126     _.each(manifest.routes || [], function (routeDetails) {
127       var verb = (routeDetails.verb || "all").toLowerCase(),
128         func = require(X.path.join(getExtensionDir(extension),
129           "node-datasource", routeDetails.filename))[routeDetails.functionName];
130
131       if (_.contains(["all", "get", "post", "patch", "delete"], verb)) {
132         app[verb]('/:org/' + routeDetails.path, func);
133       } else {
134         console.log("Invalid verb for extension-defined route " + routeDetails.path);
135       }
136     });
137   };
138
139   schemaSessionOptions.username = X.options.databaseServer.user;
140   schemaSessionOptions.database = X.options.datasource.databases[0];
141   // XXX note that I'm not addressing an underlying bug that we don't wait to
142   // listen on the port until all the setup is done
143   schemaSessionOptions.success = function () {
144     if (!SYS) {
145       return;
146     }
147     var extensions = new SYS.ExtensionCollection();
148     extensions.fetch({
149       database: X.options.datasource.databases[0],
150       success: function (coll, results, options) {
151         if (!app) {
152           // XXX time bomb: assuming app has been initialized, below, by now
153           XT.log("Could not load extension routes or client-side code because the app has not started");
154           process.exit(1);
155           return;
156         }
157         useClientDir("/client", "../enyo-client/application");
158         _.each(results, loadExtensionServerside);
159         _.each(results, loadExtensionClientside);
160       }
161     });
162   };
163   XT.session.loadSessionObjects(XT.session.SCHEMA, schemaSessionOptions);
164
165   privSessionOptions.username = X.options.databaseServer.user;
166   privSessionOptions.database = X.options.datasource.databases[0];
167   XT.session.loadSessionObjects(XT.session.PRIVILEGES, privSessionOptions);
168
169 }());
170
171
172 /**
173   Grab the version number from the package.json file.
174  */
175
176 var packageJson = X.fs.readFileSync("../package.json");
177 X.versions = {
178   core: JSON.parse(packageJson).version
179 };
180
181 /**
182  * Module dependencies.
183  */
184 var passport = require('passport'),
185   oauth2 = require('./oauth2/oauth2'),
186   routes = require('./routes/routes'),
187   socketio = require('socket.io'),
188   url = require('url'),
189   utils = require('./oauth2/utils'),
190   user = require('./oauth2/user'),
191   destroySession;
192
193 // TODO - for testing. remove...
194 //http://stackoverflow.com/questions/13091037/node-js-heap-snapshots-and-google-chrome-snapshot-viewer
195 //var heapdump = require("heapdump");
196 // Use it!: https://github.com/c4milo/node-webkit-agent
197 //var agent = require('webkit-devtools-agent');
198
199 /**
200  * ###################################################
201  * Overrides section.
202  *
203  * Sometimes we need to change how an npm packages works.
204  * Don't edit the packages directly, override them here.
205  * ###################################################
206  */
207
208 /**
209   Define our own authentication criteria for passport. Passport itself defines
210   its authentication function here:
211   https://github.com/jaredhanson/passport/blob/master/lib/passport/http/request.js#L74
212   We are stomping on that method with our own special business logic.
213   The ensureLoggedIn function will not need to be changed, because that calls this.
214  */
215 require('http').IncomingMessage.prototype.isAuthenticated = function () {
216   "use strict";
217
218   var creds = this.session.passport.user;
219
220   if (creds && creds.id && creds.username && creds.organization) {
221     return true;
222   } else {
223     destroySession(this.sessionID, this.session);
224     return false;
225   }
226 };
227
228 // Stomping on express/connect's Cookie.prototype to only update the expires property
229 // once a minute. Otherwise it's hit on every session check. This cuts down on chatter.
230 // See more details here: https://github.com/senchalabs/connect/issues/670
231 require('express/node_modules/connect/lib/middleware/session/cookie').prototype.__defineSetter__("expires", require('./stomps/expires').expires);
232
233 // Stomp on Express's cookie serialize() to not send an "expires" value to the browser.
234 // This makes the browser cooke a "session" cookie that will never expire and only
235 // gets removed when the user closes the browser. We still set express.session.cookie.maxAge
236 // below so our persisted session gets an expires value, but not the browser cookie.
237 // See this issue for more details: https://github.com/senchalabs/connect/issues/328
238 require('express/node_modules/cookie').serialize = require('./stomps/cookie').serialize;
239
240 // Stomp on Connect's session.
241 // https://github.com/senchalabs/connect/issues/641
242 function stompSessionLoad() {
243   "use strict";
244   return require('./stomps/session');
245 }
246 require('express/node_modules/connect').middleware.__defineGetter__('session', stompSessionLoad);
247 require('express/node_modules/connect').__defineGetter__('session', stompSessionLoad);
248 require('express').__defineGetter__('session', stompSessionLoad);
249
250 /**
251  * ###################################################
252  * END Overrides section.
253  * ###################################################
254  */
255
256 //
257 // Load the ssl data
258 //
259 var sslOptions = {};
260
261 sslOptions.key = X.fs.readFileSync(X.options.datasource.keyFile);
262 if (X.options.datasource.caFile) {
263   sslOptions.ca = _.map(X.options.datasource.caFile, function (obj) {
264     "use strict";
265
266     return X.fs.readFileSync(obj);
267   });
268 }
269 sslOptions.cert = X.fs.readFileSync(X.options.datasource.certFile);
270
271 /**
272  * Express configuration.
273  */
274 app = express();
275
276 var server = X.https.createServer(sslOptions, app),
277   parseSignedCookie = require('express/node_modules/connect').utils.parseSignedCookie,
278   //MemoryStore = express.session.MemoryStore,
279   XTPGStore = require('./oauth2/db/connect-xt-pg')(express),
280   io,
281   //sessionStore = new MemoryStore(),
282   sessionStore = new XTPGStore({ hybridCache: X.options.datasource.requireCache || false }),
283   Session = require('express/node_modules/connect/lib/middleware/session').Session,
284   Cookie = require('express/node_modules/connect/lib/middleware/session/cookie'),
285   cookie = require('express/node_modules/cookie'),
286   privateSalt = X.fs.readFileSync(X.options.datasource.saltFile).toString() || 'somesecret';
287
288 // Conditionally load express.session(). REST API endpoints using OAuth tokens do not get sessions.
289 var conditionalExpressSession = function (req, res, next) {
290   "use strict";
291
292   var key;
293
294   // REST API endpoints start with "/api" in their path.
295   // The 'assets' folder and login page are sessionless.
296   if ((/^api/i).test(req.path.split("/")[2]) ||
297       (/^\/assets/i).test(req.path) ||
298       req.path === "/" ||
299       req.path === "/favicon.ico" ||
300       req.path === "/forgot-password" ||
301       req.path === '/node_modules/jquery/jquery.js' ||
302       req.path === "/recover") {
303
304     next();
305   } else {
306     if (req.path === "/login") {
307       // TODO - Add check against X.options database array
308       key = req.body.database + ".sid";
309     } else if (req.path.split("/")[1]) {
310       key = req.path.split("/")[1] + ".sid";
311     } else {
312       // TODO - Dynamically name the cookie after the database.
313       console.log("### FIX ME ### setting cookie name to 'connect.sid' for path = ", JSON.stringify(req.path));
314       console.log("### FIX ME ### cookie name should match database name!!!");
315       console.trace("### At this location ###");
316       key = 'connect.sid';
317     }
318
319     // Instead of doing app.use(express.session()) we call the package directly
320     // which returns a function (req, res, next) we can call to do the same thing.
321     var init_session = express.session({
322         key: key,
323         store: sessionStore,
324         secret: privateSalt,
325         // See cookie stomp above for more details on how this session cookie works.
326         cookie: {
327           path: '/',
328           httpOnly: true,
329           secure: true,
330           maxAge: (X.options.datasource.sessionTimeout * 60 * 1000) || 3600000
331         },
332         sessionIDgen: function () {
333           // TODO: Stomp on connect's sessionID generate.
334           // https://github.com/senchalabs/connect/issues/641
335           return key.split(".")[0] + "." + utils.generateUUID();
336         }
337       });
338
339     init_session(req, res, next);
340   }
341 };
342
343 // Conditionally load passport.session(). REST API endpoints using OAuth tokens do not get sessions.
344 var conditionalPassportSession = function (req, res, next) {
345   "use strict";
346
347   // REST API endpoints start with "/api" in their path.
348   // The 'assets' folder and login page are sessionless.
349   if ((/^api/i).test(req.path.split("/")[2]) ||
350     (/^\/assets/i).test(req.path) ||
351     req.path === "/" ||
352     req.path === "/favicon.ico"
353     ) {
354
355     next();
356   } else {
357     // Instead of doing app.use(passport.session())
358     var init_passportSessions = passport.session();
359
360     init_passportSessions(req, res, next);
361   }
362 };
363
364 app.configure(function () {
365   "use strict";
366
367   // gzip all static files served.
368   app.use(express.compress());
369   // Add a basic view engine that will render files from "views" directory.
370   app.set('view engine', 'ejs');
371
372   // TODO - This outputs access logs like apache2 and some other user things.
373   //app.use(express.logger());
374
375   app.use(express.cookieParser());
376   app.use(express.bodyParser());
377
378   // Conditionally load session packages. Based off these examples:
379   // http://stackoverflow.com/questions/9348505/avoiding-image-logging-in-express-js/9351428#9351428
380   // http://stackoverflow.com/questions/13516898/disable-csrf-validation-for-some-requests-on-express
381   app.use(conditionalExpressSession);
382   app.use(passport.initialize());
383   app.use(conditionalPassportSession);
384
385   app.use(app.router);
386   app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
387 });
388
389 /**
390  * Passport configuration.
391  */
392 require('./oauth2/passport');
393
394 /**
395  * Setup HTTP routes and handlers.
396  */
397 var that = this;
398
399 app.use(express.favicon(__dirname + '/views/login/assets/favicon.ico'));
400 app.use('/assets', express.static('views/login/assets', { maxAge: 86400000 }));
401 app.use('/node_modules/jquery', express.static('../node_modules/jquery/dist', { maxAge: 86400000 }));
402
403 app.get('/:org/dialog/authorize', oauth2.authorization);
404 app.post('/:org/dialog/authorize/decision', oauth2.decision);
405 app.post('/:org/oauth/token', oauth2.token);
406
407 app.get('/:org/discovery/v1alpha1/apis/v1alpha1/rest', routes.restDiscoveryGetRest);
408 app.get('/:org/discovery/v1alpha1/apis/:model/v1alpha1/rest', routes.restDiscoveryGetRest);
409 app.get('/:org/discovery/v1alpha1/apis', routes.restDiscoveryList);
410
411 app.get('/:org/api/userinfo', user.info);
412
413 app.post('/:org/api/v1alpha1/services/:service/:id', routes.restRouter);
414 app.all('/:org/api/v1alpha1/resources/:model/:id', routes.restRouter);
415 app.all('/:org/api/v1alpha1/resources/:model', routes.restRouter);
416 app.all('/:org/api/v1alpha1/resources/*', routes.restRouter);
417
418 app.get('/', routes.loginForm);
419 app.post('/login', routes.login);
420 app.get('/forgot-password', routes.forgotPassword);
421 app.post('/recover', routes.recoverPassword);
422 app.get('/:org/recover/reset/:id/:token', routes.verifyRecoverPassword);
423 app.post('/:org/recover/resetUpdate', routes.resetRecoveredPassword);
424 app.get('/login/scope', routes.scopeForm);
425 app.post('/login/scopeSubmit', routes.scope);
426 app.get('/logout', routes.logout);
427 app.get('/:org/logout', routes.logout);
428 app.get('/:org/app', routes.app);
429 app.get('/:org/debug', routes.debug);
430
431 app.all('/:org/credit-card', routes.creditCard);
432 app.all('/:org/change-password', routes.changePassword);
433 app.all('/:org/client/build/client-code', routes.clientCode);
434 app.all('/:org/email', routes.email);
435 app.all('/:org/export', routes.exxport);
436 app.get('/:org/file', routes.file);
437 app.get('/:org/generate-report', routes.generateReport);
438 app.all('/:org/install-extension', routes.installExtension);
439 app.get('/:org/locale', routes.locale);
440 app.all('/:org/oauth/generate-key', routes.generateOauthKey);
441 app.get('/:org/reset-password', routes.resetPassword);
442 app.post('/:org/oauth/revoke-token', routes.revokeOauthToken);
443 app.all('/:org/vcfExport', routes.vcfExport);
444
445
446 // Set up the other servers we run on different ports.
447
448 var redirectServer = express();
449 redirectServer.get(/.*/, routes.redirect); // RegEx for "everything"
450 redirectServer.listen(X.options.datasource.redirectPort, X.options.datasource.bindAddress);
451
452 /**
453  * Start the express server. This is the NEW way.
454  */
455 // TODO - Active browser sessions can make calls to this server when it hasn't fully started.
456 // That can cause it to crash at startup.
457 // Need a way to get everything loaded BEFORE we start listening.  Might just move this to the end...
458 io = socketio.listen(server.listen(X.options.datasource.port, X.options.datasource.bindAddress));
459
460 X.log("Server listening at: ", X.options.datasource.bindAddress);
461 X.log("node-datasource started on port: ", X.options.datasource.port);
462 X.log("redirectServer started on port: ", X.options.datasource.redirectPort);
463 X.log("Databases accessible from this server: \n", JSON.stringify(X.options.datasource.databases, null, 2));
464
465
466 /**
467  * Destroy a single session.
468  * @param {Object} val - Session object.
469  * @param {String} key - Session id.
470  */
471 destroySession = function (key, val) {
472   "use strict";
473
474   var sessionID;
475
476   // Timeout socket.
477   if (val && val.socket && val.socket.id) {
478     _.each(io.sockets.sockets, function (sockVal, sockKey, sockList) {
479       if (val.socket.id === sockKey) {
480         _.each(sockVal.manager.namespaces, function (spaceVal, spaceKey, spaceList) {
481           sockVal.flags.endpoint = spaceVal.name;
482           // Tell the client it timed out. This will redirect the client to /logout
483           // which will destroy the session, but we can't rely on the client for that.
484           sockVal.emit("timeout");
485         });
486
487         // Disconnect socket.
488         sockVal.disconnect();
489       }
490     });
491   }
492
493   sessionID = key.replace(sessionStore.prefix, '');
494
495   // Destroy session here incase the client never hits /logout.
496   sessionStore.destroy(sessionID, function (err) {
497     //X.debug("Session destroied: ", key, " error: ", err);
498   });
499 };
500
501 // TODO - Use NODE_ENV flag to switch between development and production.
502 // See "Understanding the configure method" at:
503 // https://github.com/LearnBoost/Socket.IO/wiki/Configuring-Socket.IO
504 io.configure(function () {
505   "use strict";
506
507   io.set('log', false);
508   // TODO - We need to implement a store for this if we run multiple processes:
509   // https://github.com/LearnBoost/socket.io/tree/0.9/lib/stores
510   //http://stackoverflow.com/questions/9267292/examples-in-using-redisstore-in-socket-io/9275798#9275798
511   //io.set('store', someNewStore);                // Use our someNewStore.
512   io.set('browser client minification', true);  // Send minified file to the client.
513   io.set('browser client etag', true);          // Apply etag caching logic based on version number
514   // TODO - grubmle - See prototype stomp above:
515   // https://github.com/LearnBoost/socket.io/issues/932
516   // https://github.com/LearnBoost/socket.io/issues/984
517   io.set('browser client gzip', true);          // gzip the file.
518   //io.set('log level', 1);                       // Reduce logging.
519   io.set('transports', [                        // Enable all transports.
520       'websocket',
521       'htmlfile',
522       'xhr-polling',
523       'jsonp-polling'
524     ]
525   );
526 });
527
528 /**
529  * Setup socket.io routes and handlers.
530  *
531  * Socket.io authorization modeled off of:
532  * https://github.com/LearnBoost/socket.io/wiki/Authorizing
533  * http://stackoverflow.com/questions/13095418/how-to-use-passport-with-express-and-socket-io
534  * https://github.com/jfromaniello/passport.socketio
535  */
536 io.of('/clientsock').authorization(function (handshakeData, callback) {
537   "use strict";
538
539   var key;
540
541   if (handshakeData.headers.cookie) {
542     handshakeData.cookie = cookie.parse(handshakeData.headers.cookie);
543
544     if (handshakeData.headers.referer && url.parse(handshakeData.headers.referer).path.split("/")[1]) {
545       key = url.parse(handshakeData.headers.referer).path.split("/")[1];
546     } else if (X.options.datasource.testDatabase) {
547       // for some reason zombie doesn't send the referrer in the socketio call
548       // https://groups.google.com/forum/#!msg/socket_io/MPpXrP5N9k8/xAyk1l8Iw8YJ
549       key = X.options.datasource.testDatabase;
550     } else {
551       return callback(null, false);
552     }
553
554
555     if (!handshakeData.cookie[key + '.sid']) {
556       return callback(null, false);
557     }
558
559     // Add sessionID so we can use it to check for valid sessions on each request below.
560     handshakeData.sessionID = parseSignedCookie(handshakeData.cookie[key + '.sid'], privateSalt);
561
562     sessionStore.get(handshakeData.sessionID, function (err, session) {
563       if (err) {
564         return callback(err);
565       }
566
567       // All requests get a session. Make sure the session is authenticated.
568       if (!session || !session.passport || !session.passport.user ||
569         !session.passport.user.id ||
570         !session.passport.user.organization ||
571         !session.passport.user.username ||
572         !session.cookie || !session.cookie.expires) {
573
574         destroySession(handshakeData.sessionID, session);
575
576         // Not an error exactly, but the cookie is invalid. The user probably logged off.
577         return callback(null, false);
578       }
579
580       // Prep the cookie and create a session object so we can touch() it on each request below.
581       session.cookie.expires = new Date(session.cookie.expires);
582       session.cookie = new Cookie(session.cookie);
583       handshakeData.session = new Session(handshakeData, session);
584
585       // Add sessionStore here so it can be used to lookup valid session on each request below.
586       handshakeData.sessionStore = sessionStore;
587
588       // Move along.
589       callback(null, true);
590     });
591   } else {
592     callback(null, false);
593   }
594 }).on('connection', function (socket) {
595   "use strict";
596
597   var ensureLoggedIn = function (callback, payload) {
598         socket.handshake.sessionStore.get(socket.handshake.sessionID, function (err, session) {
599           var expires,
600               current;
601
602           // All requests get a session. Make sure the session is authenticated.
603           if (err || !session || !session.passport || !session.passport.user ||
604               !session.passport.user.id ||
605               !session.passport.user.organization ||
606               !session.passport.user.username ||
607               !session.cookie || !session.cookie.expires) {
608
609             return destroySession(socket.handshake.sessionID, session);
610           }
611
612           // Make sure the sesion hasn't expired yet.
613           expires = new Date(session.cookie.expires);
614           current = new Date();
615           if (expires <= current) {
616             return destroySession(socket.handshake.sessionID, session);
617           } else {
618             // User is still valid
619
620             // Update session expiration timeout, unless this is an automated call of
621             // some sort (e.g. lock refresh)
622             if (!payload || !payload.automatedRefresh) {
623               socket.handshake.session.touch().save();
624             }
625
626             // Move along.
627             callback(session);
628           }
629         });
630       };
631
632   // Save socket.id to the session store so we can disconnect that socket server side
633   // when a session is timed out. That should notify the client imediately they have timed out.
634   socket.handshake.session.socket = {id: socket.id};
635   socket.handshake.session.save();
636
637   // To run this from the client:
638   // ???
639   socket.on('session', function (data, callback) {
640     ensureLoggedIn(function (session) {
641       var callbackObj = X.options.client || {};
642       callbackObj = _.extend(callbackObj,
643         {
644           data: session.passport.user,
645           code: 1,
646           debugging: X.options.datasource.debugging,
647           emailAvailable: _.isString(X.options.datasource.smtpHost) && X.options.datasource.smtpHost !== "",
648           printAvailable: _.isString(X.options.datasource.printer) && X.options.datasource.printer !== "",
649           versions: X.versions
650         });
651       callback(callbackObj);
652     }, data && data.payload);
653   });
654
655   // To run this from the client:
656   socket.on('delete', function (data, callback) {
657     ensureLoggedIn(function (session) {
658       routes.queryDatabase("delete", data.payload, session, callback);
659     }, data && data.payload);
660   });
661
662   // To run this from the client:
663   // XT.dataSource.request(m = new XM.Contact(), "get", {nameSpace: "XM", type: "Contact", id: "1"}, {propagate: true, parse: true, success: function () {console.log("success", arguments)}, error: function () {console.log("error", arguments);}});
664   socket.on('get', function (data, callback) {
665     ensureLoggedIn(function (session) {
666       routes.queryDatabase("get", data.payload, session, callback);
667     }, data && data.payload);
668   });
669
670   // To run this from the client:
671   socket.on('patch', function (data, callback) {
672     ensureLoggedIn(function (session) {
673       routes.queryDatabase("patch", data.payload, session, callback);
674     }, data && data.payload);
675   });
676
677   // To run this from the client:
678   socket.on('post', function (data, callback) {
679     ensureLoggedIn(function (session) {
680       routes.queryDatabase("post", data.payload, session, callback);
681     }, data && data.payload);
682   });
683
684   // Tell the client it's connected.
685   socket.emit("ok");
686 });
687
688 /**
689  * Job loading section.
690  *
691  * The following are jobs that must be started at start up or scheduled to run periodically.
692  */
693
694 // TODO - Check pid file to see if this is already running.
695 // Kill process or create new pid file.
696
697 // Run the expireSessions cleanup/garbage collection once a minute.
698 setInterval(function () {
699     "use strict";
700
701     //X.debug("session cleanup called at: ", new Date());
702     sessionStore.expireSessions(destroySession);
703   }, 60000);