codenautas/qa-control-server

View on GitHub
lib/qac-services.js

Summary

Maintainability
F
1 wk
Test Coverage
"use strict";

var qacServices={};

var app = require('express')();
var crypto = require('crypto');
var fs = require('fs-extra');
var Path = require('path');
var OS = require('os');
var bodyParser = require('body-parser');
var execToHtml = require('exec-to-html');
var qaControl = require('qa-control');
var loginPlus = new (require('login-plus').Manager);
var qcsCommon = require('./qcs-common.js');
var miniTools = require('mini-tools');
var html = require('js-to-html').html;

// var request = require('request-promise');
var url = require('url');
var http = require('http');
var https = require('https');

// promised version de request
function request(params) {
    return new Promise(function(resolve, reject) {
        var options=url.parse(params.uri);
        options.headers=params.headers;
        var req = (options.protocol.match(/https/)?https:http).request(options, function(res) {
            var data=[];
            res.setEncoding('utf8');
            res.on('data', function(chunk) { data.push(chunk); });
            res.on('end', function() { resolve(data.join('')); });
        });
        req.on('error', function(err) { reject(err); });
        req.end();
    });
}

html.insecureModeEnabled = true;

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended:true}));

qacServices = {
    production: true,
    repository: {
        path : './repositories',
        request_secret: 'HK39Sas_D--lld#h./@'
    },
    rootUrl:'/'
};

qacServices.config = function(opts, production){
    if(production == null){
        throw new Error("must set 'production' in config");
    }
    if(opts && opts.repository) {
        qacServices.repository = opts.repository;
    }
    if(opts && opts["root-url"]) {
        qacServices.rootUrl = opts["root-url"];
    }
    qacServices.production = !!production;
};

qacServices.cucardasToHtmlList = function cucardasToHtmlList(cucardasMd) {
    var e_link = '([^)]+)';
    var e_img = '!\\[([^\\]]+)]\\('+e_link+'\\)';
    var e_cucarda = '(?:'+e_img+'|\\['+e_img+']\\('+e_link+'\\))';
    var cucardasHtml = [];
    cucardasMd.replace(new RegExp(e_cucarda,'g'),function(cucarda, name, imgSrc, name2, imgSrc2, url){
        var htmlCucarda;
        name = name || name2;
        htmlCucarda = html.img({src:imgSrc||imgSrc2, alt:name});
        if(url){
            htmlCucarda = html.a( {href:url}, [htmlCucarda]);
        }
        var position=qacServices.cucardasToHtmlList.order[name]||qacServices.cucardasToHtmlList.order._OTHERS_;
        while(cucardasHtml.length<position){
            cucardasHtml.push([]);
        }
        cucardasHtml[position-1].push(htmlCucarda);
    });
    while(cucardasHtml.length<qacServices.cucardasToHtmlList.order._OTHERS_){
        /* istanbul ignore next */
        cucardasHtml.push([]);
    }
    return cucardasHtml.map(function(cucardasOnePosition){
        return html.td({class:'centrado'}, cucardasOnePosition);
    });
};

qacServices.cucardasToHtmlList.order={
    stable:1, 
    extending:1, 
    designing:1,
    training:1,
    "proof-of-concept":1,
    example:1,
    "npm-version":2,
    downloads:3,
    build:4,
    linux:4,
    windows:5,
    coverage:6,
    climate:7,
    dependencies:8,
    issues:9,
    "qa-control": 10,
    _OTHERS_: 11
}

qacServices.orgNameToHtmlLink = function orgNameToHtmlLink(organization) {
    return html.a({href:qacServices.rootUrl+organization}, organization);
};

qacServices.projectNameToHtmlLink = function projectNameToHtmlLink(organization, project) {
    return html.a({href:'https://github.com/'+organization+'/'+project}, project);
};

qacServices.setSession = function setSession(req) {
    var users = qacServices.users ? qacServices.users : {};
    var connectSID = req && req.cookies ? req.cookies['connect.sid'] : null;
    if(connectSID && req.session && req.session.passport && req.session.passport.user) {
        users[connectSID] = req.session.passport.user;
    }
    return users;
};

qacServices.validSession = function validSession(req) {
    var connectSID = req && req.cookies ? req.cookies['connect.sid'] : null;
    return qacServices.users && connectSID && connectSID in qacServices.users;
};

// parameters:
//  organization: name of organization
//       project: optional project name
qacServices.getInfo = function getInfo(organization, project){
    var info={};
    return Promise.resolve().then(function() {
        if(!organization) {
            throw new Error('missing organization');
        }
        info.organization = {
            path:Path.normalize(qacServices.repository.path+'/groups/'+organization),
            name:organization
        };
        info.organization.projectsJsonPath = Path.normalize(info.organization.path+'/params/projects.json');
        return fs.stat(info.organization.path).catch(function(err) {
            if(err.code==='ENOENT') {
                var err2=new Error('inexistent organization "'+organization+'"');
                err2.statusCode=404;
                throw err2;
            }
            throw err;
        }).then(function(st) {
            if(!st.isDirectory()) {
                throw new Error('invalid organization "'+organization+'"');
            }
        }).then(function() {
            return fs.readFile(info.organization.projectsJsonPath,'utf8');
        }).then(JSON.parse).then(function(projects) {
            info.organization.projects = projects;
            if(!!project) {
                var projectFound=projects.filter(function(element, index, array) {
                    return element.projectName==project;
                });
                if(! projectFound.length) {
                    var err2 = new Error('inexistent project "'+project+'"');
                    err2.statusCode=404;
                    throw err2;
                }
                info.project = {
                    path:Path.normalize(info.organization.path+'/projects/'+project),
                    name:project
                };
                return fs.stat(info.project.path).catch(function(err) {
                    if(err.code !== 'ENOENT') { throw err; }
                    else { return {isDirectory: function() { return false; }}; }
                }).then(function(st) {
                    if(!st.isDirectory()) {
                        throw new Error('invalid project "'+project+'"');
                    }
                    return info;
                });
            }
        }).then(function() {
            return info;
        });
    });
};

qacServices.orgActionButtons = function orgActionButtons(req, organization){
    var tds=[];
    if(qacServices.validSession(req)) {
        tds.push(html.td({class:'centrado'}, [
            html.a({
                href: qacServices.rootUrl+'ask/delete/'+organization,
                'codenautas-confirm': 'row'
            },[html.img({src:qacServices.rootUrl+'delete.png', alt:'del', style:'height:18px'})])
        ]))
    }
    return tds;
}

qacServices.projectActionButtons = function projectActionButtons(req, organization, project){
    var tds=[];
    if(qacServices.validSession(req)) {
        tds.push(html.td([
            html.a({
                href: qacServices.rootUrl+'ask/delete/'+organization+'/'+project,
                'codenautas-confirm': 'row'
            },[html.img({src:qacServices.rootUrl+'delete.png', alt:'del', style:'height:18px'})])
        ]))
    }
    if(qacServices.validSession(req)){
        tds.push(html.td([
            html.a({
                href:qacServices.rootUrl+'refresh/'+organization+'/'+project
            }, [html.img({src:qacServices.rootUrl+'refresh.png', alt:'rfrsh', style:'height:18px'})])
        ]));
    }
    return tds;
}

qacServices.orgAddButton = function orgAddButton(req){
    var ret=[];
    if(qacServices.validSession(req)) {
        ret.push(
            html.input({type:'hidden', name:'action',       value:'create'}),
            html.input({type:'text',   name:'organization'}),
            html.input({type:'submit', value:'New organization...'})
        )
    }
    return ret;
};

qacServices.projectAddButton = function projectAddButton(req, organization){
    var ret=[];
    if(qacServices.validSession(req)) {
        ret.push(
            html.input({type:'hidden', name:'action',       value:'add'}),
            html.input({type:'hidden', name:'organization', value:organization}),
            html.input({type:'text',   name:'project'}),
            html.input({type:'submit', value:'New project...'})
        )
    }
    return ret;
}

qacServices.getProject = function getProject(req, info, project) {
    var organization = info.organization.name;
    return fs.readFile(Path.normalize(info.organization.path+'/projects/'+project.projectName+'/result/cucardas.md'), 'utf8').catch(function(err) {
        if(err.code !== 'ENOENT') { throw err; }
        return qacServices.invalidSVG();
    }).then(function(content){
        if(!/\[issues\]/.test(content)){
            content += '[![issues](https://img.shields.io/github/issues-raw/'+organization+'/'+project.projectName+'.svg)](https://github.com/'+organization+'/'+project.projectName+'/issues)';
        }
        if(!/\[qa-control\]/.test(content)){
            content += '[![qa-control]('+(qacServices.rootUrl+organization+'/')+project.projectName+'.svg)]('+(qacServices.rootUrl+organization+'/')+project.projectName+')';
        }
        var tds = qacServices.cucardasToHtmlList(content);
        tds.unshift(html.td([qacServices.projectNameToHtmlLink(organization, project.projectName)]));
        tds = tds.concat(qacServices.projectActionButtons(req, organization, project.projectName));
        return html.tr(tds);
    });
};

qacServices.sortProjects = function sortProjects(proj1, proj2) {
    var v1=proj1.content[0].content[0].attributes['href'],
        v2=proj2.content[0].content[0].attributes['href'];
    if(v1 < v2) {
        return -1;
    } else if(v1 > v2) {
        return 1;
    }
    return 0;
}

qacServices.getOrganizationPage = function getOrganizationPage(req, organization){
    return qacServices.getInfo(organization).then(function(info) {
        if(info.organization.projects.length) {
            return Promise.all(info.organization.projects.map(function(project) {
                return qacServices.getProject(req, info, project);
            })).then(function(trs) {
                trs = trs.sort(qacServices.sortProjects);
                var tds = [ html.th('project'), html.th({colspan:qacServices.cucardasToHtmlList.order._OTHERS_-1},'cucardas')];
                if(qacServices.validSession(req)) {
                    tds.push(html.th({colspan:4},'actions'));
                }
                var all_trs = [ html.tr(tds)];
                for(var tr in trs) { all_trs.push(trs[tr]); }
                if(qacServices.validSession(req)) {
                    all_trs.push(html.tr([html.td({colspan:qacServices.cucardasToHtmlList.order._OTHERS_+(1+4), "class":'right-align'}, qacServices.projectAddButton(req, organization))]));
                }
                if(qacServices.validSession(req)) {
                    return html.form({method:'post', action:qacServices.rootUrl}, [html.table(all_trs)]);
                } else {
                    return html.table(all_trs);
                }
            });
        } else {
            if(qacServices.validSession(req)) {
                return html.form({method:'post', action:qacServices.rootUrl}, [html.table([html.tr([html.td(qacServices.projectAddButton(req, organization))])])]);
            } else {
                return html.table([html.tr([html.td(organization+' has no projects')])]);
            }
            
        }
    });
};

qacServices.getProjectLogs = function getProjectLogs(projectPath) {
    var r=[];
    return fs.readJSON(Path.normalize(projectPath+'/result/qa-control-result.json'), 'utf8').catch(function(err) {
        if(err.code !== 'ENOENT') { throw err; }
        return [];
    }).then(function(qac) {
        if(qac.length) {
            var trs=[];
            for(var b in qac) {
                var reg = qac[b];
                trs.push(
                    html.tr([
                        html.td([reg.warning]),
                        html.td([reg.params ? reg.params.join(',') : '']),
                        html.td([JSON.stringify(reg.scoring||'').replace(/[,]/g,' ').replace(/[{}"]/g,'')]),
                    ])
                );
            }
            trs.unshift(html.tr([ html.td('warning'), html.td('file'), html.td('scoring') ]));
            trs.unshift(html.tr([html.th({colspan:3}, 'QA Control result')]));
            r.push(html.hr());
            r.push(html.table(trs));
        }
        return fs.readJSON(Path.normalize(projectPath+'/result/bitacora.json'), 'utf8');
    }).catch(function(err) {
        if(err.code !== 'ENOENT') { throw err; }
        return [];
    }).then(function(bita) {
        if(bita.length) {
            var trs=[];
            for(var b in bita) {
                var reg = bita[b];
                var cls = 'stdout';
                if(reg.origin==='internal') {
                    cls = 'internal';
                } else if(reg.origin.match(/^(shell)/)) {
                    cls = 'shell';
                }
                trs.push(html.tr([
                            html.td([html.div({class:cls}, reg.text.trim())])
                                 ])
                        );
            }
            trs.unshift(html.tr([ html.th('Actions log') ]));
            r.push(html.hr());
            r.push(html.table(trs));
        }
        return r;
    });
};

qacServices.getProjectPage = function getProjectPage(req, organization, project){
    var pageCont;
    var info;
    return qacServices.getInfo(organization, project).then(function(nfo) {
        info = nfo;
        return qacServices.getProject(req, info, {projectName:project});
    }).then(function(projTR) {
        var tds = [ html.th('project'), html.th({colspan:qacServices.cucardasToHtmlList.order._OTHERS_-1},'cucardas')];
        if(qacServices.validSession(req)) {
            tds.push(html.th({colspan:4},'actions'));
        }
        var all_trs = [ html.tr(tds), projTR];
        if(qacServices.validSession(req)) {
            return html.form({method:'post', action:qacServices.rootUrl}, html.table(all_trs));
        } else {
            return html.table(all_trs);
        }
    }).then(function(content) {
        pageCont = content;
        return qacServices.getProjectLogs(info.project.path);
    }).then(function(logs) {
        if(logs.length) {
            logs.unshift(pageCont);
            return logs;
        }
        return [pageCont];
    }).then(function(content) {
        return html.body(content);
    }).catch(function(err) {
        console.log("getProjectPage err", err);
        console.log("getProjectPage stack", err.stack);
        throw err;
    });
};

qacServices.getAdminPage = function getAdminPage(req){
    var newOrgTR = html.tr([html.td({colspan:2, "class":'right-align'}, qacServices.orgAddButton(req))]);
    return qacServices.getOrganizations().then(function(orgs) {
        if(orgs.length) {
            return Promise.all(orgs.map(function(org) {
                var tds = [];
                tds.unshift(html.td([qacServices.orgNameToHtmlLink(org)]));
                tds = tds.concat(qacServices.orgActionButtons(req, org));
                return html.tr(tds);
            })).then(function(trs) {
                var tds = [html.th('organization')];
                if(qacServices.validSession(req)) {
                    tds.push(html.th('actions'));
                }
                var all_trs = [ html.tr(tds)];
                for(var tr in trs) { all_trs.push(trs[tr]); }
                if(qacServices.validSession(req)) {
                    all_trs.push(newOrgTR);
                }
                if(qacServices.validSession(req)) {
                    return html.form({method:'post', action:qacServices.rootUrl}, [html.table(all_trs)]);
                } else {
                    return html.table(all_trs);
                }
            });
        } else {
            if(qacServices.validSession(req)) {
                return html.form({method:'post', action:qacServices.rootUrl}, [html.table([newOrgTR])]);
            } else {
                return html.table([html.tr([html.td('There are no organizations')])]);
            }
        }
    });
};

function json2file(filePath, jsonData) {
    return fs.writeFile(filePath, JSON.stringify(jsonData, null, 4), {encoding:'utf8'});
}

function Bitacora(pathInfo, pathBita) {
    this.data = [];
    this.now = function() {
        var d=new Date().toJSON().replace(/:/g,'').replace('T','_');//.replace(/-/g, '');
        return d.substr(0, d.length-5);
    };
    this.logAll = function(type, data) {
        this.data.push({date:this.now(), type:type, data:data});
    };
    this.log = function(origin, text) {
        this.data.push({date:this.now(), origin:origin, text:text});
    };
    this.finish = function() {
        function isBitacora(obj) { return 'origin' in obj; }
        function isAll(obj) { return ! isBitacora(obj); }
        var logs = this.data;
        var fileBita = logs.filter(isBitacora);
        var vNow = this.now();
        json2file(pathBita+'bitacora.json', fileBita).then(function() {
            var fileAll = logs.filter(isAll);
            return json2file(pathInfo+'bitacora_'+vNow+'.json', fileAll);
        }).catch(function(err) {
            console.log("bitacora ERROR", err.stack);
        });
    };
};

qacServices.getResource = function getResource(name) {
    return fs.readFile('./resources/'+name, 'utf8').then(function(isvg) {
       return isvg;
    });
};

qacServices.invalidSVG = function invalidSVG() {
    return qacServices.getResource('qa--control-invalid-lightgrey.svg');
};
qacServices.naSVG = function naSVG() {
    return qacServices.getResource('qa--control-na-lightgrey.svg');
};

var reOrg = /^([a-zA-Z][a-zA-Z0-9_-]+)$/i;

qacServices.createOrganization = function createOrganization(name) {
    var orgPath = Path.normalize(qacServices.repository.path+'/groups/'+name);
    var dirs = [
        Path.normalize(orgPath+'/params'),
        Path.normalize(orgPath+'/projects')
    ];
    return Promise.resolve().then(function() {
        if(!name) { throw new Error('missing organization name'); }
        if(! name.match(reOrg)) {
            throw new Error('invalid organization name "'+name+'"');
        }
        return fs.mkdir(orgPath);
    }).then(function(){
        return Promise.all(dirs.map(function(dir) {
            return fs.mkdir(dir);
        })).then(function() {
            var projecsJS = Path.normalize(orgPath+'/params/projects.json');
            var auxi=fs.writeJson(projecsJS, [], {});
            return auxi.then(function(){
            // return fs.writeJson(projecsJS, []).then(function() {
                return 'organization "' + name +'" created';
            });
        });
    }).catch(function(err){
        if(err.code=='EEXIST'){
            throw new Error('cannot create existing organization "'+name+'" it already exists');
        }
        throw err;
    });
};

/*
    ver https://developer.github.com/v3/#rate-limiting
    Cuando se supera este limite github devuelve 403 y esta funcion falla
    porque el request no es autenticado y el limite es de 60 requests por hora.
    Implementando auth seria de 5000 requests por hora
*/
qacServices.existsOnGithub = function existsOnGithub(organization, project) {
    return Promise.resolve().then(function() {
        var params = {uri:'https://api.github.com/repos/'+organization+'/'+project, headers: { 'User-Agent': 'Request-Promise' }};
        return request(params);
    }).then(function(repo) {
        try{
            var proj = JSON.parse(repo);
        }catch(err){
            console.log('JSON.ERR ',organization+'/'+project, repo);
        }
        if(proj.message && proj.message.match(/not found/i)) {
            return {projNotFound:true};
        }
        return {};
    }).catch(function(err) {
        if(err.statusCode && err.statusCode !== 404) {
            throw new Error('github validation error', err.message);
        }
        console.log(err);
        console.log(err.stack);
        return {projNotFound:true};
    });
};

qacServices.createProject = function createProject(organization, project) {
    var info;
    var projPath;
    var projects;
    return Promise.resolve().then(function() {
        if(!organization) { throw new Error('missing organization name'); }
        if(!project) { throw new Error('missing project name'); }
        if(! organization.match(reOrg)) {
            throw new Error('invalid organization name "'+organization+'"');
        }
        if(! project.match(reOrg)) {
            throw new Error('invalid project name "'+project+'"');
        }
        return qacServices.getInfo(organization);
    }).then(function(nfo) {
        info = nfo;
        projects = info.organization.projects;
        var projectFound=projects.filter(function(element, index, array) {
            return element.projectName==project;
        });
        if(projectFound.length) {
            throw new Error('duplicate project "'+project+'"');
        }
        return qacServices.existsOnGithub(organization, project);
    }).then(function(eog) {
        if(eog.orgNotFound) {
            throw new Error('inexistent organization on github ', organization);
        }
        if(eog.projNotFound) {
            throw new Error('inexistent project on github ', project);
        }
    }).then(function() {
        projects.push({projectName:project});
        return fs.writeJSON(info.organization.projectsJsonPath, projects);
    }).then(function() {
        projPath = Path.normalize(info.organization.path+'/projects/'+project);
        return fs.mkdir(projPath);
    }).then(function() {
        var folders=['result', 'info', 'params'/*, 'source'*/];
        return Promise.all(folders.map(function(folder) {
            return fs.mkdir(Path.normalize(projPath+'/'+folder));
        }));
    }).then(function() {
        return 'project "'+project+'" created';
    });
};

qacServices.deleteData = function deleteData(organization, project){
    return qacServices.getInfo(organization, project).then(function(info) {
        if(!!project) {
            var dirToRemove=Path.normalize(info.project.path);
            info.organization.projects = info.organization.projects.filter(function(p) {
                return p.projectName !== project;
            });
            return fs.writeJSON(info.organization.projectsJsonPath, info.organization.projects).then(function() {
                return fs.remove(dirToRemove);
            }).then(function(){
                return 'project "' + project +'" removed';
            });
        } else {
            var dirToRemove=Path.normalize(info.organization.path);
            return fs.remove(dirToRemove).then(function(){
                return 'organization "' + organization +'" removed';
            });
        }
    }).catch(function(err) {
        return err.message;
    });
};

qacServices.receivePush = function receivePush(){
    return app.post(qacServices.rootUrl+'push/:organization/:project',function receivePushService(req,res){
        var eventType=req.headers['x-github-event'];
        if(!eventType){
            res.status(400);
            res.end('bad request. Missing X-GitHub-Event header');
            return;
        }
        // validar request
        var githubSig = req.headers['x-hub-signature'];
        if(githubSig && ! qacServices.isValidRequest(JSON.stringify(req.body), githubSig, qacServices.repository.request_secret)) {
            res.status(403);
            res.end('unauthorized request. Invalid x-hub-signature');
            return;
        }
        // guardar en base de datos
        var repo=req.body.repository;
        if(!repo.organization){ // for de ping event
            repo.organization = repo.full_name.split('/')[0];
        }
        actualizeRepo(repo, res, (req.body.head_commit||{}).timestamp||(req.body.repository.pushed_at)||Date(), false);
    });
};

qacServices.receiveManualPush = function receiveManualPush(){
    // http://localhost:7226/refresh/codenautas/multilang?url=https://github.com/codenautas/multilang
    return app.get(qacServices.rootUrl+'refresh/:organization/:project',function receiveManualPushService(req,res){
        // validar request
        var repo={
            organization: req.params.organization, 
            name:         req.params.project,
            html_url:     req.query.url || 'https://github.com/'+req.params.organization+'/'+req.params.project
        };
        actualizeRepo(repo, res, Date(), true);
    });
};

function Feedback(res) {
    res.setHeader('Connection', 'Transfer-Encoding');
    res.setHeader('Content-Type', 'text/html; charset=utf-8');
    //res.setHeader('Content-Type', 'text/plain; charset=utf-8');
    //res.setHeader('Content-Type', 'text/plain');
    res.setHeader('Transfer-Encoding', 'chunked');
    //res.write('Pushing manually...<br>\n');
    this.msg = function(data) {
        res.write(data+'<br>\n');
    }
};

function NoFeedback() { this.msg = function() {} };

qacServices.doRepoUpdate = function doRepoUpdate(info, repo_url, feedback, bitacora) {
    var clonePath = Path.normalize(info.project.path+'/source');
    var resultsPath = Path.normalize(info.project.path+'/result');
    var cucardasFile = Path.normalize(clonePath+'/cucardas.log');
    var qaControlWarnings;
    
    return fs.stat(clonePath).then(function() {
        return true;
    }).catch(function(err) {
        if(err.code !== 'ENOENT') { 
            if(bitacora){
                bitacora.logAll('internal: exception', err);
                bitacora.finish();
            }
            throw err;
        }
        return false;
    }).then(function(mustDelete) {
        if(mustDelete){
            return fs.remove(clonePath);
        }
    }).then(function(){
        return execToHtml.run([{
            command:'git',
            params:['clone', '-q', repo_url+'.git', clonePath]
        }],{echo:true, exit:true}).onLine(function(lineInfo){
            feedback.msg(lineInfo.text);
            bitacora.log(lineInfo.origin, lineInfo.text);
        }).catch(function(err){
            console.log('-------------',err);
            throw err;
        });
    }).then(function() {
        return qaControl.controlProject(clonePath, {verbose:false, cucardas:true});
    }).then(function(warns) {
        qaControlWarnings = warns;
        bitacora.logAll('internal: qa-control',warns);
        bitacora.log('internal', 'qa-control-result: '+JSON.stringify(warns));
        return json2file(Path.normalize(resultsPath+'/qa-control-result.json'), warns);
    }).then(function() {
        return fs.stat(cucardasFile).then(function(){
            return true;
        }).catch(function(err) {
            if(!err || err.code !== 'ENOENT') { throw err; }
            return false;
        });
    }).then(function(haveCucardas) {
        if(haveCucardas) {
            var cucardasMD = Path.normalize(resultsPath+'/cucardas.md');
            return fs.readFile(cucardasFile, {encoding:'utf8'}).then(function(content) {
                var cucardas = content.split('\n').splice(1);
                return fs.writeFile(cucardasMD, cucardas.join('\n'), {encoding:'utf8'});
            }).then(function() {
                bitacora.log('internal', '"'+cucardasMD+'" generated');
            });
        } else {
            bitacora.logAll('internal', 'Sin cucardas!!!');
        }
    }).then(function(){
        // procesar las warnings de qa-control
        var gravities = {
            error  :{count:0, label:'% err', color:'red'   },
            obs    :{count:0, label:'% obs', color:'yellow'},
            warning:{count:0, label:'% war', color:'orange'},
            notice :{count:0, label:'ok', color:'green'}, 
        }
        var scoreTypes = {
            cucardas   :{gravity:'warning'},
            multilang  :{gravity:'warning'},
            warning    :{gravity:'warning'},
            jshint     :{gravity:'obs'    },
            eslint     :{gravity:'obs'    },
            notice     :{gravity:'notice'},
            repository :{gravity:'error'  },
            conventions:{gravity:'error'  },
            mandatories:{gravity:'error'  },
            fatal      :{gravity:'error'  },
        }
        var qaControlLastVersion=true;
        qaControlWarnings.forEach(function(warn){
            warn.scoring = warn.scoring || {fatal:1};
            for(var scoreName in warn.scoring){
                gravities[(scoreTypes[scoreName]||scoreTypes.fatal).gravity].count+=warn.scoring[scoreName];
            }
        })
        var label;
        var color;
        for(var gravity in gravities){
            var county = gravities[gravity];
            if(!label && county.count){
                label=county.label.replace('%', county.count);
                color=county.color;
            }
        }
        if(!label){
            label='ok';
            color = 'brightgreen';
        }
        return request({uri:'https://img.shields.io/badge/qa--control-'+label+'-'+color+'.svg'});
    }).then(function(resp) {
        return fs.writeFile(Path.normalize(resultsPath+'/cucarda.svg'), resp, {encoding:'utf8'});
    }).then(function() {
        bitacora.finish();
    });
};

function bitacoraFor(info) {
    return new Bitacora(Path.normalize(info.project.path+'/info/'), Path.normalize(info.project.path+'/result/'));
};

function actualizeRepo(repo, res, timestamp, isManual){
    var info;
    var feedback = isManual ? new Feedback(res) : new NoFeedback();
    var bitacora;
    qacServices.getInfo(repo.organization, repo.name).then(function(nfo) {
        info = nfo;
        bitacora = bitacoraFor(info);   
        return qacServices.doRepoUpdate(nfo, repo.html_url, feedback, bitacora);
    }).then(function() {
        return qacServices.getProjectLogs(info.project.path);
    }).then(function(logs) {
        if(! isManual) {
            res.end('ok: '+timestamp);
        } else {
            var content = html.html([qcsCommon.simpleHead('result.css', qacServices), html.body(logs)]);
            res.end(content.toHtmlDoc({pretty:true, title:repo.organization+' - '+repo.name+' qa-control'}));
        }
    }).catch(function(err) {
        console.log("qac-services.actualizeRepo err", err);
        console.log(err.stack);
        if(bitacora){
            bitacora.logAll('internal: exception', err);
            bitacora.finish();
        }
        if(err.statusCode){
            res.status(err.statusCode);
            res.end(err.message);
        }else{
            res.status(500);
            res.end("fatal error");
        }
    });
};

qacServices.getOrganizations = function getOrganizations(){
    var organizations=[];
    var repoPath = Path.normalize(qacServices.repository.path+'/groups');
    return Promise.resolve().then(function() {
        return fs.readdir(repoPath).catch(function(err) {
            if(err.code==='ENOENT') {
                var err2 = new Error('inexistent repository "'+repoPath+'"');
                err2.statusCode=404;
                throw err2;
            }
            throw err;
        }).then(function(files) {
            return Promise.all(files.map(function(file) {
                var fullPath = Path.normalize(repoPath+'/'+file);
                return fs.stat(fullPath).then(function(stat) {
                    if(stat.isDirectory()) {
                        organizations.push(file);
                    }
                });
            }));
        }).then(function() {
            return organizations.sort();
        });
    });
};

qacServices.isValidRequest = function isValidRequest(payload, keyInHeader, secret) {
    var hmac = crypto.createHmac('sha1', secret);
    hmac.setEncoding('hex');
    hmac.write(payload);
    hmac.end();
    var check = 'sha1='+hmac.read().toString('hex');
    var rv = check===keyInHeader;
    return rv;
};

qacServices.staticServe = function staticServe(){
    return app.get(qacServices.rootUrl+':filename',function(req,res,next){
        if(req.params.filename.match(/(\.(css|jpg|png|gif|ico|js))$/)) {
            return res.sendFile(Path.resolve('./app/'+req.params.filename));
        }
        return next();
    });
};

qacServices.serveSVG = function serveSVG(organization, project){
    var project = project.substring(0, project.length-4);
    return qacServices.getInfo(organization, project).then(function(info) {
        return fs.readFile(Path.normalize(info.project.path+'/result/cucarda.svg'), 'utf8');
    }).catch(function(err) {
        if(err.code !== 'ENOENT') { throw err; }
        return qacServices.invalidSVG();
    }).then(function(isvg) {
        return isvg;
    });
};

qacServices.addParam = function addParam(elems, elem, name) {
    if(elem) { elems.push(html.input({type:'hidden', name:name, value:elem}));  }
};

qacServices.askServe = function askServe(){
    var thisModule = this;
    return app.use(thisModule.rootUrl+'ask',function(req,res){
        var elems = [
                    html.p({style:'color:gray'}, thisModule.rootUrl),
                    html.h2(["Proceed with: "+req.path+" ?"])
                ];
        var vars = req.path.split('/');
        qacServices.addParam(elems, vars[1], 'action');
        qacServices.addParam(elems, vars[2], 'organization');
        qacServices.addParam(elems, vars[3], 'project');
        elems.push(html.input({type:'submit', value:'Ok'}));
        var o=html.form({method:'post', action:thisModule.rootUrl},elems);
        miniTools.serveText(o.toHtmlDoc({title:'please confirm'}),'html')(req,res);
    });
};

qacServices.uriIsHandled = function uriIsHandled(req) {
    return req.params.organization.match(/^(login|admin|(manual-)?(delete|create|add))$/);
}

qacServices.noCacheHeaders = function noCacheHeaders(res, textType) {
    res.header('Cache-Control', 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0');
    if(textType){
        res.header('Content-Type', 'text/'+textType+'; charset=utf-8');
    }
};

qacServices.organizationServe = function organizationServe(req){
    var thisModule = this;
    return app.get(thisModule.rootUrl+':organization',function(req,res,next){
        if(qacServices.uriIsHandled(req)) {
            return next();
        } else {
            thisModule.getOrganizationPage(req, req.params.organization).then(function(content){
                qacServices.noCacheHeaders(res, 'html');
                content = html.html([qcsCommon.simpleHead(null, qacServices), content]);
                res.end(content.toHtmlDoc({pretty:true, title:req.params.organization+' qa-control'}));
            }).catch(function(err) {
                console.log("organizationServe err", err);
                console.log("organizationServe stack", err.stack);
                res.statusCode=err.statusCode||500;
                res.end(res.statusCode+" Internal error:"+(!thisModule.production?err.message:''));
            }); 
        }
    });
};

qacServices.projectServe = function projectServe(){
    var thisModule = this;
    return app.get(thisModule.rootUrl+':organization/:project',function(req,res,next){
        if(qacServices.uriIsHandled(req)) {
            return next();
        } else {
            var isSvg=req.params.project.match(/(.svg)$/);
            var args = [req.params.organization, req.params.project];
            var action;
            if(isSvg) {
                action = thisModule.serveSVG;
            } else {
                args.unshift(req);
                action = thisModule.getProjectPage;
            }
            action.apply(this, args).then(function(content){
                qacServices.noCacheHeaders(res);
                if(isSvg) {
                    res.setHeader('Content-Type', 'image/svg+xml');
                    res.end(content);
                } else {
                    res.setHeader('Content-Type', 'text/html');
                    content = html.html([qcsCommon.simpleHead('result.css', qacServices), content]);
                    res.end(content.toHtmlDoc({pretty:true, title:req.params.organization+' - '+req.params.project+' qa-control'}));
                }
            }).catch(function(err) {
                return qacServices.naSVG().then(function(svg) {
                    res.setHeader('Content-Type', 'image/svg+xml');
                    res.end(svg);
                });
            }); 
        }
    });
};

qacServices.createAndUpdateProject = function createAndUpdateProject(organization, project, feedback) {
    var creteProjMsg;
    return qacServices.createProject(organization, project).then(function(cpm) {
        feedback.msg(cpm);
        creteProjMsg = cpm;
        return qacServices.getInfo(organization, project);
    }).then(function(info) {
        return qacServices.doRepoUpdate(info, 'https://github.com/'+organization+'/'+project, feedback, bitacoraFor(info));
    }).then(function() {
       return creteProjMsg; 
    }).catch(function(err) {
        console.log("doRepoUpdate err", err.stack);
        throw err;
    });
};

function handleAbms(thisModule, url, method, pref) {
    this.url = url;
    this.handle = function(req, res, next) {
        var vars = method === 'post' ? req.body : req.params;
        if(req.session===undefined){
            console.log('****************** req.session undefined');
        }
        thisModule.users = thisModule.setSession(req);
        var doAction=null;
        switch(vars.action) {
            case pref+'delete': doAction = thisModule.deleteData; break;
            case pref+'add': doAction = thisModule.createAndUpdateProject; break;
            case pref+'create': doAction = thisModule.createOrganization;  break; // ignora vars.project
        }
        if(! doAction) { return next(); }
        doAction(vars.organization, vars.project, pref !== '' ? new Feedback(res) : new NoFeedback()).then(function(content) {
            res.end(content); 
        }).catch(function(err) {
            res.end(err.message)
        });
    };
};

qacServices.abmsManualServe = function abmsManualServe() {
    console.log("------------- abmsManualServe --------------");
    var handler = new handleAbms(this, qacServices.rootUrl+':action/:organization/:project?', 'get', 'manual-');
    return app.get(handler.url, handler.handle);
};

qacServices.md5Prefixed = function md5Prefixed(text){
    return 'md5.'+crypto.createHash('md5').update(text).digest('hex');
};

qacServices.enableLoginPlus = function enableLoginPlus(usersDatabasePath) {
    if(! usersDatabasePath) {
        throw new Error('must provide path to users database');
    }
    if(! fs.existsSync(usersDatabasePath)) {
        throw new Error('users database not found ['+usersDatabasePath+']');
    }
    loginPlus.init(app,{
        baseUrl:'/github',
        successRedirect:'/admin',
        allowHttpLogin:true,
        loginForm:{
            formImg:'login.png',
            formTitle: 'qa-control log in'
        }
        /*
        successRedirect:qacServices.rootUrl+'admin',
        unloggedPath:Path.normalize(__dirname+'/../app'),
        loginPagePath:Path.normalize(__dirname+'/../app/login'),
        loginUrlPath:qacServices.rootUrl+'login',
        */
    });
    loginPlus.setValidatorStrategy(
        function(req, username, password, done) {
            var users;
            fs.readJson(usersDatabasePath).then(function(json) {
                users = json;
            }).then(function() {
                var user = users[username];
                if(!!user && ! user.locked && user.pass == qacServices.md5Prefixed(password+username)) {
                    done(null, {username: username, when: Date()});
                } else {
                    done('Unauthorized');
                }
            }).catch(function(err){
                console.log('error logueando',err);
                console.log('stack',err.stack);
                throw err;
            }).catch(done);
        }
    );
};

qacServices.abmsServe = function abmsServe() {
    var handler = new handleAbms(this, qacServices.rootUrl, 'post', '');
    return app.post(handler.url, handler.handle);
};

qacServices.adminServe = function adminServe(){
    var thisModule = this;
    return app.get(qacServices.rootUrl+'admin',function(req,res,next){
        thisModule.users = thisModule.setSession(req);
        thisModule.getAdminPage(req).then(function(content){
            qacServices.noCacheHeaders(res, 'html');
            content = html.html([qcsCommon.simpleHead(null, thisModule), content]);
            res.end(content.toHtmlDoc({pretty:true, title:'admin qa-control'}));
        }).catch(function(err) {
            console.log("adminServe err", err);
            console.log("adminServe stack", err.stack);
            res.statusCode=err.statusCode||500;
            res.end(res.statusCode+" Internal error:"+(!thisModule.production?err.message:''));
        }); 
    });
};

module.exports=qacServices;