Subversion Repositories Applications.papyrus

Rev

Blame | Last modification | View Log | RSS feed

if(!dojo._hasResource["dojox._sql.common"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dojox._sql.common"] = true;
dojo.provide("dojox._sql.common");

dojo.require("dojox._sql._crypto");

// summary:
//      Executes a SQL expression.
// description:
//      There are four ways to call this:
//      1) Straight SQL: dojox.sql("SELECT * FROM FOOBAR");
//      2) SQL with parameters: dojox.sql("INSERT INTO FOOBAR VALUES (?)", someParam)
//      3) Encrypting particular values: 
//                      dojox.sql("INSERT INTO FOOBAR VALUES (ENCRYPT(?))", someParam, "somePassword", callback)
//      4) Decrypting particular values:
//                      dojox.sql("SELECT DECRYPT(SOMECOL1), DECRYPT(SOMECOL2) FROM
//                                      FOOBAR WHERE SOMECOL3 = ?", someParam,
//                                      "somePassword", callback)
//
//      For encryption and decryption the last two values should be the the password for
//      encryption/decryption, and the callback function that gets the result set.
//
//      Note: We only support ENCRYPT(?) statements, and
//      and DECRYPT(*) statements for now -- you can not have a literal string
//      inside of these, such as ENCRYPT('foobar')
//
//      Note: If you have multiple columns to encrypt and decrypt, you can use the following
//      convenience form to not have to type ENCRYPT(?)/DECRYPT(*) many times:
//
//      dojox.sql("INSERT INTO FOOBAR VALUES (ENCRYPT(?, ?, ?))", 
//                                      someParam1, someParam2, someParam3, 
//                                      "somePassword", callback)
//
//      dojox.sql("SELECT DECRYPT(SOMECOL1, SOMECOL2) FROM
//                                      FOOBAR WHERE SOMECOL3 = ?", someParam,
//                                      "somePassword", callback)
dojox.sql = new Function("return dojox.sql._exec(arguments);");

dojo.mixin(dojox.sql, {
        dbName: null,
        
        // summary:
        //      If true, then we print out any SQL that is executed
        //      to the debug window
        debug: (dojo.exists("dojox.sql.debug")?dojox.sql.debug:false),

        open: function(dbName){
                if(this._dbOpen && (!dbName || dbName == this.dbName)){
                        return;
                }
                
                if(!this.dbName){
                        this.dbName = "dot_store_" 
                                + window.location.href.replace(/[^0-9A-Za-z_]/g, "_");
                        //console.debug("Using Google Gears database " + this.dbName);
                }
                
                if(!dbName){
                        dbName = this.dbName;
                }
                
                try{
                        this._initDb();
                        this.db.open(dbName);
                        this._dbOpen = true;
                }catch(exp){
                        throw exp.message||exp;
                }
        },

        close: function(dbName){
                // on Internet Explorer, Google Gears throws an exception
                // "Object not a collection", when we try to close the
                // database -- just don't close it on this platform
                // since we are running into a Gears bug; the Gears team
                // said it's ok to not close a database connection
                if(dojo.isIE){ return; }
                
                if(!this._dbOpen && (!dbName || dbName == this.dbName)){
                        return;
                }
                
                if(!dbName){
                        dbName = this.dbName;
                }
                
                try{
                        this.db.close(dbName);
                        this._dbOpen = false;
                }catch(exp){
                        throw exp.message||exp;
                }
        },
        
        _exec: function(params){
                try{    
                        // get the Gears Database object
                        this._initDb();
                
                        // see if we need to open the db; if programmer
                        // manually called dojox.sql.open() let them handle
                        // it; otherwise we open and close automatically on
                        // each SQL execution
                        if(!this._dbOpen){
                                this.open();
                                this._autoClose = true;
                        }
                
                        // determine our parameters
                        var sql = null;
                        var callback = null;
                        var password = null;

                        var args = dojo._toArray(params);

                        sql = args.splice(0, 1)[0];

                        // does this SQL statement use the ENCRYPT or DECRYPT
                        // keywords? if so, extract our callback and crypto
                        // password
                        if(this._needsEncrypt(sql) || this._needsDecrypt(sql)){
                                callback = args.splice(args.length - 1, 1)[0];
                                password = args.splice(args.length - 1, 1)[0];
                        }

                        // 'args' now just has the SQL parameters

                        // print out debug SQL output if the developer wants that
                        if(this.debug){
                                this._printDebugSQL(sql, args);
                        }

                        // handle SQL that needs encryption/decryption differently
                        // do we have an ENCRYPT SQL statement? if so, handle that first
                        if(this._needsEncrypt(sql)){
                                var crypto = new dojox.sql._SQLCrypto("encrypt", sql, 
                                                                                                        password, args, 
                                                                                                        callback);
                                return; // encrypted results will arrive asynchronously
                        }else if(this._needsDecrypt(sql)){ // otherwise we have a DECRYPT statement
                                var crypto = new dojox.sql._SQLCrypto("decrypt", sql, 
                                                                                                        password, args, 
                                                                                                        callback);
                                return; // decrypted results will arrive asynchronously
                        }

                        // execute the SQL and get the results
                        var rs = this.db.execute(sql, args);
                        
                        // Gears ResultSet object's are ugly -- normalize
                        // these into something JavaScript programmers know
                        // how to work with, basically an array of 
                        // JavaScript objects where each property name is
                        // simply the field name for a column of data
                        rs = this._normalizeResults(rs);
                
                        if(this._autoClose){
                                this.close();
                        }
                
                        return rs;
                }catch(exp){
                        exp = exp.message||exp;
                        
                        console.debug("SQL Exception: " + exp);
                        
                        if(this._autoClose){
                                try{ 
                                        this.close(); 
                                }catch(e){
                                        console.debug("Error closing database: " 
                                                                        + e.message||e);
                                }
                        }
                
                        throw exp;
                }
        },

        _initDb: function(){
                if(!this.db){
                        try{
                                this.db = google.gears.factory.create('beta.database', '1.0');
                        }catch(exp){
                                dojo.setObject("google.gears.denied", true);
                                dojox.off.onFrameworkEvent("coreOperationFailed");
                                throw "Google Gears must be allowed to run";
                        }
                }
        },

        _printDebugSQL: function(sql, args){
                var msg = "dojox.sql(\"" + sql + "\"";
                for(var i = 0; i < args.length; i++){
                        if(typeof args[i] == "string"){
                                msg += ", \"" + args[i] + "\"";
                        }else{
                                msg += ", " + args[i];
                        }
                }
                msg += ")";
        
                console.debug(msg);
        },

        _normalizeResults: function(rs){
                var results = [];
                if(!rs){ return []; }
        
                while(rs.isValidRow()){
                        var row = {};
                
                        for(var i = 0; i < rs.fieldCount(); i++){
                                var fieldName = rs.fieldName(i);
                                var fieldValue = rs.field(i);
                                row[fieldName] = fieldValue;
                        }
                
                        results.push(row);
                
                        rs.next();
                }
        
                rs.close();
                
                return results;
        },

        _needsEncrypt: function(sql){
                return /encrypt\([^\)]*\)/i.test(sql);
        },

        _needsDecrypt: function(sql){
                return /decrypt\([^\)]*\)/i.test(sql);
        }
});

// summary:
//      A private class encapsulating any cryptography that must be done
//      on a SQL statement. We instantiate this class and have it hold
//      it's state so that we can potentially have several encryption
//      operations happening at the same time by different SQL statements.
dojo.declare("dojox.sql._SQLCrypto", null, {
        constructor: function(action, sql, password, args, callback){
                if(action == "encrypt"){
                        this._execEncryptSQL(sql, password, args, callback);
                }else{
                        this._execDecryptSQL(sql, password, args, callback);
                }               
        }, 
        
        _execEncryptSQL: function(sql, password, args, callback){
                // strip the ENCRYPT/DECRYPT keywords from the SQL
                var strippedSQL = this._stripCryptoSQL(sql);
        
                // determine what arguments need encryption
                var encryptColumns = this._flagEncryptedArgs(sql, args);
        
                // asynchronously encrypt each argument that needs it
                var self = this;
                this._encrypt(strippedSQL, password, args, encryptColumns, function(finalArgs){
                        // execute the SQL
                        var error = false;
                        var resultSet = [];
                        var exp = null;
                        try{
                                resultSet = dojox.sql.db.execute(strippedSQL, finalArgs);
                        }catch(execError){
                                error = true;
                                exp = execError.message||execError;
                        }
                
                        // was there an error during SQL execution?
                        if(exp != null){
                                if(dojox.sql._autoClose){
                                        try{ dojox.sql.close(); }catch(e){}
                                }
                        
                                callback(null, true, exp.toString());
                                return;
                        }
                
                        // normalize SQL results into a JavaScript object 
                        // we can work with
                        resultSet = dojox.sql._normalizeResults(resultSet);
                
                        if(dojox.sql._autoClose){
                                dojox.sql.close();
                        }
                                
                        // are any decryptions necessary on the result set?
                        if(dojox.sql._needsDecrypt(sql)){
                                // determine which of the result set columns needs decryption
                                var needsDecrypt = self._determineDecryptedColumns(sql);

                                // now decrypt columns asynchronously
                                // decrypt columns that need it
                                self._decrypt(resultSet, needsDecrypt, password, function(finalResultSet){
                                        callback(finalResultSet, false, null);
                                });
                        }else{
                                callback(resultSet, false, null);
                        }
                });
        },

        _execDecryptSQL: function(sql, password, args, callback){
                // strip the ENCRYPT/DECRYPT keywords from the SQL
                var strippedSQL = this._stripCryptoSQL(sql);
        
                // determine which columns needs decryption; this either
                // returns the value *, which means all result set columns will
                // be decrypted, or it will return the column names that need
                // decryption set on a hashtable so we can quickly test a given
                // column name; the key is the column name that needs
                // decryption and the value is 'true' (i.e. needsDecrypt["someColumn"] 
                // would return 'true' if it needs decryption, and would be 'undefined'
                // or false otherwise)
                var needsDecrypt = this._determineDecryptedColumns(sql);
        
                // execute the SQL
                var error = false;
                var resultSet = [];
                var exp = null;
                try{
                        resultSet = dojox.sql.db.execute(strippedSQL, args);
                }catch(execError){
                        error = true;
                        exp = execError.message||execError;
                }
        
                // was there an error during SQL execution?
                if(exp != null){
                        if(dojox.sql._autoClose){
                                try{ dojox.sql.close(); }catch(e){}
                        }
                
                        callback(resultSet, true, exp.toString());
                        return;
                }
        
                // normalize SQL results into a JavaScript object 
                // we can work with
                resultSet = dojox.sql._normalizeResults(resultSet);
        
                if(dojox.sql._autoClose){
                        dojox.sql.close();
                }
        
                // decrypt columns that need it
                this._decrypt(resultSet, needsDecrypt, password, function(finalResultSet){
                        callback(finalResultSet, false, null);
                });
        },

        _encrypt: function(sql, password, args, encryptColumns, callback){
                //console.debug("_encrypt, sql="+sql+", password="+password+", encryptColumns="+encryptColumns+", args="+args);
        
                this._totalCrypto = 0;
                this._finishedCrypto = 0;
                this._finishedSpawningCrypto = false;
                this._finalArgs = args;
        
                for(var i = 0; i < args.length; i++){
                        if(encryptColumns[i]){
                                // we have an encrypt() keyword -- get just the value inside
                                // the encrypt() parantheses -- for now this must be a ?
                                var sqlParam = args[i];
                                var paramIndex = i;
                        
                                // update the total number of encryptions we know must be done asynchronously
                                this._totalCrypto++;
                        
                                // FIXME: This currently uses DES as a proof-of-concept since the
                                // DES code used is quite fast and was easy to work with. Modify dojox.sql
                                // to be able to specify a different encryption provider through a 
                                // a SQL-like syntax, such as dojox.sql("SET ENCRYPTION BLOWFISH"),
                                // and modify the dojox.crypto.Blowfish code to be able to work using
                                // a Google Gears Worker Pool
                        
                                // do the actual encryption now, asychronously on a Gears worker thread
                                dojox._sql._crypto.encrypt(sqlParam, password, dojo.hitch(this, function(results){
                                        // set the new encrypted value
                                        this._finalArgs[paramIndex] = results;
                                        this._finishedCrypto++;
                                        // are we done with all encryption?
                                        if(this._finishedCrypto >= this._totalCrypto
                                                && this._finishedSpawningCrypto){
                                                callback(this._finalArgs);
                                        }
                                }));
                        }
                }
        
                this._finishedSpawningCrypto = true;
        },

        _decrypt: function(resultSet, needsDecrypt, password, callback){
                //console.debug("decrypt, resultSet="+resultSet+", needsDecrypt="+needsDecrypt+", password="+password);
                
                this._totalCrypto = 0;
                this._finishedCrypto = 0;
                this._finishedSpawningCrypto = false;
                this._finalResultSet = resultSet;
        
                for(var i = 0; i < resultSet.length; i++){
                        var row = resultSet[i];
                
                        // go through each of the column names in row,
                        // seeing if they need decryption
                        for(var columnName in row){
                                if(needsDecrypt == "*" || needsDecrypt[columnName]){
                                        this._totalCrypto++;
                                        var columnValue = row[columnName];
                                
                                        // forming a closure here can cause issues, with values not cleanly
                                        // saved on Firefox/Mac OS X for some of the values above that
                                        // are needed in the callback below; call a subroutine that will form 
                                        // a closure inside of itself instead
                                        this._decryptSingleColumn(columnName, columnValue, password, i,
                                                                                                function(finalResultSet){
                                                callback(finalResultSet);
                                        });
                                }
                        }
                }
        
                this._finishedSpawningCrypto = true;
        },

        _stripCryptoSQL: function(sql){
                // replace all DECRYPT(*) occurrences with a *
                sql = sql.replace(/DECRYPT\(\*\)/ig, "*");
        
                // match any ENCRYPT(?, ?, ?, etc) occurrences,
                // then replace with just the question marks in the
                // middle
                var matches = sql.match(/ENCRYPT\([^\)]*\)/ig);
                if(matches != null){
                        for(var i = 0; i < matches.length; i++){
                                var encryptStatement = matches[i];
                                var encryptValue = encryptStatement.match(/ENCRYPT\(([^\)]*)\)/i)[1];
                                sql = sql.replace(encryptStatement, encryptValue);
                        }
                }
        
                // match any DECRYPT(COL1, COL2, etc) occurrences,
                // then replace with just the column names
                // in the middle
                matches = sql.match(/DECRYPT\([^\)]*\)/ig);
                if(matches != null){
                        for(var i = 0; i < matches.length; i++){
                                var decryptStatement = matches[i];
                                var decryptValue = decryptStatement.match(/DECRYPT\(([^\)]*)\)/i)[1];
                                sql = sql.replace(decryptStatement, decryptValue);
                        }
                }
        
                return sql;
        },

        _flagEncryptedArgs: function(sql, args){
                // capture literal strings that have question marks in them,
                // and also capture question marks that stand alone
                var tester = new RegExp(/([\"][^\"]*\?[^\"]*[\"])|([\'][^\']*\?[^\']*[\'])|(\?)/ig);
                var matches;
                var currentParam = 0;
                var results = [];
                while((matches = tester.exec(sql)) != null){
                        var currentMatch = RegExp.lastMatch+"";

                        // are we a literal string? then ignore it
                        if(/^[\"\']/.test(currentMatch)){
                                continue;
                        }

                        // do we have an encrypt keyword to our left?
                        var needsEncrypt = false;
                        if(/ENCRYPT\([^\)]*$/i.test(RegExp.leftContext)){
                                needsEncrypt = true;
                        }

                        // set the encrypted flag
                        results[currentParam] = needsEncrypt;

                        currentParam++;
                }
        
                return results;
        },

        _determineDecryptedColumns: function(sql){
                var results = {};

                if(/DECRYPT\(\*\)/i.test(sql)){
                        results = "*";
                }else{
                        var tester = /DECRYPT\((?:\s*\w*\s*\,?)*\)/ig;
                        var matches;
                        while(matches = tester.exec(sql)){
                                var lastMatch = new String(RegExp.lastMatch);
                                var columnNames = lastMatch.replace(/DECRYPT\(/i, "");
                                columnNames = columnNames.replace(/\)/, "");
                                columnNames = columnNames.split(/\s*,\s*/);
                                dojo.forEach(columnNames, function(column){
                                        if(/\s*\w* AS (\w*)/i.test(column)){
                                                column = column.match(/\s*\w* AS (\w*)/i)[1];
                                        }
                                        results[column] = true;
                                });
                        }
                }

                return results;
        },

        _decryptSingleColumn: function(columnName, columnValue, password, currentRowIndex,
                                                                                        callback){
                //console.debug("decryptSingleColumn, columnName="+columnName+", columnValue="+columnValue+", currentRowIndex="+currentRowIndex)
                dojox._sql._crypto.decrypt(columnValue, password, dojo.hitch(this, function(results){
                        // set the new decrypted value
                        this._finalResultSet[currentRowIndex][columnName] = results;
                        this._finishedCrypto++;
                        
                        // are we done with all encryption?
                        if(this._finishedCrypto >= this._totalCrypto
                                && this._finishedSpawningCrypto){
                                //console.debug("done with all decrypts");
                                callback(this._finalResultSet);
                        }
                }));
        }
});

}