routes/doc.js (539 lines of code) (raw):

const express = require('express'); const fs = require('fs'); const ObjectID = require('mongodb').ObjectID; const docModel = require('../models/doc'); const conf = require('../config/conf'); const querymw = require('../lib/querymw'); const package = require('../package.json'); const csurf = require('csurf'); // ASF const asf = require('../custom/asf.js'); // END ASF var csrfProtection = csurf(); var querymen = require('querymen'); var qs = require('querystring'); const _ = require('lodash'); const path = require('path'); const mongoose = require('mongoose'); var queryMW; module.exports = function (name, opts) { opts.schemaName = name; //todo make it configurable var idpath = opts.idpath = opts.facet.ID.path; if (undefined == opts.facet.ID.link) { opts.facet.ID.href = '/' + name + '/'; } var jsonidpath = opts.jsonidpath = idpath.substr(5); var idpattern = opts.idpattern = opts.facet.ID.regex; //console.log('ID pattern' +idpattern); var project = {}; var columns = []; var tabFacet = {}; var bulkInput = {}; var toIndex = {}; var defaultSort = {}; var lookups = []; var chartFacet = { count: [{ $count: "total" }] }; var chartCount = 0; for (key in opts.facet) { var options = opts.facet[key]; //toIndex[options.path] = options.sort ? options.sort : 1; if (!options.hideColumn) { if (options.project) { project[key] = options.project; } else if (Array.isArray(options.path)) { project[key] = { "$setUnion": [options.path.map(x => { return '$' + x })] } } else if (typeof options.path === 'string') { project[key] = '$' + options.path; } else if (Object.keys(options.path).length != 0) { project[key] = options.path; } columns.push(key); } if (options.tabs) { toIndex[options.path] = options.sort ? options.sort : 1; if (Array.isArray(options.pipeline)) { tabFacet[key] = options.pipeline; } else { tabFacet[key] = [{ $sortByCount: '$' + options.path }]; } if (options.sort) { tabFacet[key].push({ $sort: { _id: options.sort } }) } } if (options.chart) { chartCount++; toIndex[options.path] = options.sort ? options.sort : 1; if (Array.isArray(options.pipeline)) { chartFacet[key] = options.pipeline; } else { chartFacet[key] = [{ $sortByCount: '$' + options.path }]; } if (options.sort) { chartFacet[key].push({ $sort: { _id: options.sort } }) } } if (options.bulk) { if (options.enum) { bulkInput[key] = { type: 'select', enum: options.enum } } else { bulkInput[key] = { type: 'input' } } } if (options.lookup) { //console.log('OL:'+JSON.stringify(options.lookup)); lookups = lookups.concat(options.lookup); } } queryMW = querymw(opts.facet); var module = {}; var Document = module.Document = docModel(name); //console.log(toIndex); for (var x in toIndex) { var o = {}; o[x] = toIndex[x]; delete o.createIndex; Document.collection.createIndex(o, { background: true }).catch(function(e){ console.log('Error ensuring text index: ' + e.message) }); } var router; if (opts.router) { router = opts.router; } else { router = express.Router(); } router.get('*', function (req, res, next) { res.locals.schemaName = name; res.locals.page = req.baseUrl + req.path; next(); }); router.get('/json/:id', function (req, res) { var ids = req.params.id.match(RegExp(idpattern, 'img')); if (ids) { var searchSchema = Document; var q = {}; q[idpath] = { "$in": ids }; searchSchema.find(q, { //body: 1, _id: 0 }, {}, function (err, docs) { if (err) { res.json({ title: 'Error', message: 'Query failed', docs: [] }); } else { res.json(docs); } }); } else { res.json([]); } }); router.post('/json/', async function (req, res) { if (req.body.ids && req.body.ids.length > 0) { //console.log('REQ: ' + JSON.stringify(req.body.ids)); var q = {}; q[idpath] = { "$in": req.body.ids }; var fields = { _id: 0 }; if (req.body.fields && req.body.fields.length > 0) { for (var f of req.body.fields) { fields[f] = 1; } } var results = await Document.find(q, fields); res.json(results); } else { res.json([]); } }); /* router.get('/comment/:id(' + idpattern + ')', async function (req, res) { var q = {}; q[idpath] = req.params.id; var ret = await Document.findOne(q, {comments: 1}).exec(); var emails = await Document.db.collection('mails').find({'$text':{'$search': '"' + req.params.id + '"'}},{'author':1,'subject':1,'body':1,'html':1,'createdAt':1,_id:0}).toArray(); //res.json(ret ? ret.comments.sort(function(a, b) {return a.createdAt < b.createdAt;}) : []); //console.log(emails); res.json(unified); }); */ /* replaced with lodash get var deep_value = function (obj, path) { var ret = obj; for (var i = 0, path = path.split('.'), len = path.length; i < len; i++) { ret = ret[path[i]]; if (ret === undefined) { break; } }; //console.log(' = ' + ret); return ret; }; */ router.get('/list/', queryMW, async function (req, res) { var r = await Document.aggregate([ { $match: req.querymen.query }, { $project: project } ]); res.json(r); }); router.get('/:t(examples|enum)/', queryMW, async function (req, res) { //console.log(JSON.stringify(req.querymen.query)); var r = await Document.find(req.querymen.query).distinct(req.query.field); var ret = {}; ret[req.params.t] = r; res.json(ret); }); router.get('/agg/', queryMW, async function (req, res) { if (req.query.f) { var f = req.query.f; if (!Array.isArray(f)) { f = [f]; } var pipeLine = normalizeQuery(req.querymen.query); var prj = {}; for (var k of f) { var options = opts.facet[k]; if (options) { if (Array.isArray(options.path)) { prj[k] = { "$setUnion": [options.path.map(x => { return '$' + x })] } } else if (typeof options.path === 'string') { prj[k] = '$' + options.path; } else if (Object.keys(options.path).length != 0) { prj[k] = options.path; } } } if (Object.keys(prj).length > 0) { var g = {}, gg = {}, sor = {}; if (typeof f !== 'string' && f.length == 1) { g = '$' + f[0]; } else { for (var k of f) { g[k] = '$' + k; sor[k] = 1; gg[k] = '$_id.' + k; } } pipeLine = pipeLine.concat([{ $project: prj }, { $group: { _id: g, t: { $sum: 1 } } } ]); gg.t = '$t'; if (f[1] && !req.query.ungroup) { var s = {}; s["_id." + f[1]] = 1; pipeLine.push({ $sort: s }); delete gg[f[0]]; pipeLine.push({ $group: { _id: '$_id.' + f[0], t: { $sum: '$t' }, items: { $push: gg } } }) } if (req.querymen.cursor.sort) { pipeLine.push({ $sort: { '_id': 1 } }) } //console.log('pipeLine:' + JSON.stringify(pipeLine,2,2,2)); var ret = await Document.aggregate(pipeLine); res.json(ret); } else { res.json([{ "_id": "Error: Wrong field specification", "t": 404 }]); } } else { res.json([]); } }); function normalizeQuery(q) { //console.log('GOT' + JSON.stringify(q,2,2,2)); var pipeLine = []; if (q['$text']) { if (q['$text']['$in']) { var terms = ""; for (var term of q['$text']['$in']) { terms = terms + ' ' + term; } delete q['$text']['$in']; q['$text']['$search'] = terms; } if (q['$text']['$search']) { var ids = q['$text']['$search'].match(RegExp(idpattern, 'img')); if (ids && ids.length) { var idq = {}; q[idpath] = { "$in": ids } delete q['$text']; } } } // Translate queries for empty strings to match any of "", null, non-existant. for (var p in q) { if (q[p] && q[p]['$in'] && q[p]['$in'].includes('null')) { q[p]['$in'].push(""); q[p]['$in'].push(null); } if (q[p] === '') { q[p] = { "$not": { "$exists": true, "$nin": ['', null] } }; //{"$not":{"$exists": true, $ne: ""}} //q[p] = {"$in":["",null]}; } if (q[p] === 'null') { //req.querymen.query[q] = {"$exists": false} q[p] = { "$in": ["", null] } } } var lookups = {}; if (Array.isArray(opts.conf.lookup) && opts.conf.lookup.length > 0) { var lookupAsNames = {}; for (var l of opts.conf.lookup) { lookupAsNames[l.$lookup.as] = true; } for (var p in q) { var a = p.split('.', 1)[0]; if (lookupAsNames[a]) { lookups[p] = q[p]; delete q[p]; } } pipeLine = pipeLine.concat(opts.conf.lookup) if (Object.keys(lookups).length != 0) { pipeLine.push({ "$match": lookups }); } } pipeLine.unshift({ "$match": q }); //console.log('PIPEline' + JSON.stringify(pipeLine,2,2,2)); return pipeLine; }; /* The Main listing routine */ router.get('/', csrfProtection, queryMW, async function (req, res) { try { // ASF var mychartCount = chartCount; // END ASF var pipeLine = normalizeQuery(req.querymen.query); // to get the documents // get top level tabs aggregated counts var tabs = []; // ASF if (!asf.asfgroupacls(conf.admingroupname,req.user.pmcs)) { if (res.locals.schemaName == "cve5") { mytabFacet = {"state":[ {"$match":{"body.CNA_private.owner":{"$in":req.user.pmcs}}}, {"$group":{ _id:"$body.CNA_private.state", count: {$sum:1}}}]}; } else { mytabFacet = {"state":[ {"$match":{"body.CNA_private.owner":{"$in":req.user.pmcs}}}, {"$group":{ _id:"$body.CVE_data_meta.STATE", count: {$sum:1}}}]}; } mychartCount = 0; // ASF because we have to filter them all tabs = await Document.aggregate([{ $facet: mytabFacet }]).exec(); } else { if (Object.keys(tabFacet).length != 0) { //console.log('QUERY:' + JSON.stringify(req.querymen.query,2,3,4)); tabs = await Document.aggregate([{ $facet: tabFacet }]).exec(); } // END ASF } // get the charts aggregated counts var sort = {}; if (req.querymen.cursor.sort) { for (var s in req.querymen.cursor.sort) { if (opts.facet[s] && opts.facet[s].path) { sort[opts.facet[s].path] = req.querymen.cursor.sort[s]; } } } var allQuery = []; if (opts.conf.unwind) { allQuery = [opts.conf.unwind]; } if ((Object.keys(sort).length != 0)) { allQuery.push({ $sort: sort }); } allQuery = allQuery.concat([ { $skip: req.querymen.cursor.skip }, { $limit: req.querymen.cursor.limit }, { $project: project } ]); //console.log('AllQuery:' + JSON.stringify(allQuery,1,1,1)); var docs = []; var charts = []; var total = 0; var numCollation = { locale: "en_US", numericOrdering: true }; // ASF if (mychartCount > 0) { // END ASF chartFacet.all = allQuery; pipeLine.push({ $facet: chartFacet }); var agg = Document.aggregate(pipeLine).collation(numCollation); charts = await agg.exec(); //console.log('Aggregation QUERY: ' + JSON.stringify(pipeLine, null, 3)); docs = charts[0].all; delete charts[0].all; if (charts[0] && charts[0].count && charts[0].count[0]) { total = charts[0].count[0].total; } //console.log('Facet:' + JSON.stringify(charts,null,1)) delete charts[0].count; } else { //console.log('PROJE' + JSON.stringify(project)); total = await Document.countDocuments(req.querymen.query).exec(); var aggQuery = [ { $match: req.querymen.query }].concat(allQuery); //console.log('AGG QUERY' + JSON.stringify(aggQuery,1,1,1)); docs = await Document. aggregate(aggQuery).collation(numCollation).exec(); //total = docs.length; } //console.log('Results'+ JSON.stringify(docs,1,1,1)); // ASF filter out things you have no access to here. we could alter the query, but lets do it here var filtered = docs.filter(function(value,index,arr) { return asf.asfgroupacls(value.owner,req.user.pmcs)}); docs = filtered; total = docs.length; // END ASF var currentPage = 1; if (req.query.page) { currentPage = req.query.page; } //console.log('GOT TOTAL ' + total); var pages = Math.ceil(total / req.querymen.cursor.limit); //console.log(' PAGES = ' + currentPage) //if(charts) { //console.log('FACET:' + JSON.stringify(chartFacet, null, 2)); //} res.setHeader('Pragma', 'no-cache'); res.setHeader('Cache-Control', 'no-store, must-revalidate, max-age=0'); res.locals.renderStartTime = Date.now(); res.render(opts.list, { title: (opts.conf ? opts.conf.title + ' - ' : '') + package.name, docs: docs, opts: opts, // textUtil: textUtil, qs: qs, focustab: 0, facet: charts, tfacet: tabs, fields: opts.facet, query: req.query, limit: req.querymen.cursor.limit, pages: pages, total: total, columns: columns, current: currentPage, csrfToken: req.csrfToken(), bulkInput: bulkInput }); } catch (err) { //req.flash('error', err); res.render('blank', { title: 'Error', message: 'failed. ' + err.message }); } }); var onedoc = require('./onedoc')(Document, opts); var History = mongoose.models[opts.schemaName + '_history'] || docModel(opts.schemaName + '_history'); //UPDATE many router.post('/update', csrfProtection, function (req, res, next) { req.query = req.body; next(); }, queryMW, async function (req, res) { try { var q = req.querymen.query; var f = q[idpath]; if (f) { delete q[idpath]; for (k in q) { if (q[k] === "") { delete q[k] } } if (Object.keys(q).length != 0) { var d = new Date(); q.author = req.user.username; q.updatedAt = d; //console.log(q); var fq = {}; fq[idpath] = f; var docs = await Document.find(fq); console.log(['Bulkd', Document]) var results = []; for (var d of docs) { var result = await Document.findByIdAndUpdate( d._id, { "$set": q, "$inc": { __v: 1 } }, { "upsert": false, "new": true }); var r = onedoc.addModelHistory(History, d, result); if (r) { r.__v = r.__v + ' (' + _.get(result, idpath) + ')'; results.push(r); } } res.render('changes', { //textUtil: textUtil, title: 'Bulk update results', docs: results }); } else { res.render('blank', { title: 'Error', message: 'Error: No updates specified! Please select fields and values to update.' }); } } else { res.render('blank', { title: 'Error', message: 'Error: No items selected. Please select one or more items to update' }); } } catch (err) { req.flash('error', err); res.render('blank', { title: 'Error', message: 'failed bulk updates: ' + err.message }); } }); router.get('/render.js', function (req, res) { res.compile(opts.render, { cache: true }); }); if (opts.static) { //console.log('PATH: ' + path.join(__dirname, '/../', opts.static)); router.use('/static', express.static(path.join(__dirname, '/../', opts.static))); } // ToDo eliminate, as it can be embedded if (opts.schema) { //console.log('PATH: ' + path.join(__dirname, '/../', opts.schema)); //router.use('/schema.js', express.static(path.join(__dirname, '/../', opts.schema))); router.use('/schema.js', function (req, res) { res.setHeader('content-type', 'text/javascript'); res.send('docSchema = ' + JSON.stringify(opts.schema)); }); } var comments = require('./comments'); router.use(onedoc.router); if (opts.conf.files) { var attachment = require('./attachments'); router.use(attachment(Document, opts)); } router.use(comments(Document, opts)); module.router = router; return module; }