source: Dev/branches/rest-dojo-ui/client/dojox/sql/_base.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

File size: 15.4 KB
Line 
1dojo.provide("dojox.sql._base");
2dojo.require("dojox.sql._crypto");
3
4dojo.mixin(dojox.sql, {
5        // summary:
6        //      Executes a SQL expression.
7        // description:
8        //      There are four ways to call this:
9        //      1) Straight SQL: dojox.sql("SELECT * FROM FOOBAR");
10        //      2) SQL with parameters: dojox.sql("INSERT INTO FOOBAR VALUES (?)", someParam)
11        //      3) Encrypting particular values:
12        //                      dojox.sql("INSERT INTO FOOBAR VALUES (ENCRYPT(?))", someParam, "somePassword", callback)
13        //      4) Decrypting particular values:
14        //                      dojox.sql("SELECT DECRYPT(SOMECOL1), DECRYPT(SOMECOL2) FROM
15        //                                      FOOBAR WHERE SOMECOL3 = ?", someParam,
16        //                                      "somePassword", callback)
17        //
18        //      For encryption and decryption the last two values should be the the password for
19        //      encryption/decryption, and the callback function that gets the result set.
20        //
21        //      Note: We only support ENCRYPT(?) statements, and
22        //      and DECRYPT(*) statements for now -- you can not have a literal string
23        //      inside of these, such as ENCRYPT('foobar')
24        //
25        //      Note: If you have multiple columns to encrypt and decrypt, you can use the following
26        //      convenience form to not have to type ENCRYPT(?)/DECRYPT(*) many times:
27        //
28        //      dojox.sql("INSERT INTO FOOBAR VALUES (ENCRYPT(?, ?, ?))",
29        //                                      someParam1, someParam2, someParam3,
30        //                                      "somePassword", callback)
31        //
32        //      dojox.sql("SELECT DECRYPT(SOMECOL1, SOMECOL2) FROM
33        //                                      FOOBAR WHERE SOMECOL3 = ?", someParam,
34        //                                      "somePassword", callback)
35
36        dbName: null,
37       
38        // summary:
39        //      If true, then we print out any SQL that is executed
40        //      to the debug window
41        debug: (dojo.exists("dojox.sql.debug") ? dojox.sql.debug:false),
42
43        open: function(dbName){
44                if(this._dbOpen && (!dbName || dbName == this.dbName)){
45                        return;
46                }
47               
48                if(!this.dbName){
49                        this.dbName = "dot_store_"
50                                + window.location.href.replace(/[^0-9A-Za-z_]/g, "_");
51                        // database names in Gears are limited to 64 characters long
52                        if(this.dbName.length > 63){
53                          this.dbName = this.dbName.substring(0, 63);
54                        }
55                }
56               
57                if(!dbName){
58                        dbName = this.dbName;
59                }
60               
61                try{
62                        this._initDb();
63                        this.db.open(dbName);
64                        this._dbOpen = true;
65                }catch(exp){
66                        throw exp.message||exp;
67                }
68        },
69
70        close: function(dbName){
71                // on Internet Explorer, Google Gears throws an exception
72                // "Object not a collection", when we try to close the
73                // database -- just don't close it on this platform
74                // since we are running into a Gears bug; the Gears team
75                // said it's ok to not close a database connection
76                if(dojo.isIE){ return; }
77               
78                if(!this._dbOpen && (!dbName || dbName == this.dbName)){
79                        return;
80                }
81               
82                if(!dbName){
83                        dbName = this.dbName;
84                }
85               
86                try{
87                        this.db.close(dbName);
88                        this._dbOpen = false;
89                }catch(exp){
90                        throw exp.message||exp;
91                }
92        },
93       
94        _exec: function(params){
95                try{
96                        // get the Gears Database object
97                        this._initDb();
98               
99                        // see if we need to open the db; if programmer
100                        // manually called dojox.sql.open() let them handle
101                        // it; otherwise we open and close automatically on
102                        // each SQL execution
103                        if(!this._dbOpen){
104                                this.open();
105                                this._autoClose = true;
106                        }
107               
108                        // determine our parameters
109                        var sql = null;
110                        var callback = null;
111                        var password = null;
112
113                        var args = dojo._toArray(params);
114
115                        sql = args.splice(0, 1)[0];
116
117                        // does this SQL statement use the ENCRYPT or DECRYPT
118                        // keywords? if so, extract our callback and crypto
119                        // password
120                        if(this._needsEncrypt(sql) || this._needsDecrypt(sql)){
121                                callback = args.splice(args.length - 1, 1)[0];
122                                password = args.splice(args.length - 1, 1)[0];
123                        }
124
125                        // 'args' now just has the SQL parameters
126
127                        // print out debug SQL output if the developer wants that
128                        if(this.debug){
129                                this._printDebugSQL(sql, args);
130                        }
131
132                        // handle SQL that needs encryption/decryption differently
133                        // do we have an ENCRYPT SQL statement? if so, handle that first
134                        var crypto;
135                        if(this._needsEncrypt(sql)){
136                                crypto = new dojox.sql._SQLCrypto("encrypt", sql,
137                                                                                                        password, args,
138                                                                                                        callback);
139                                return null; // encrypted results will arrive asynchronously
140                        }else if(this._needsDecrypt(sql)){ // otherwise we have a DECRYPT statement
141                                crypto = new dojox.sql._SQLCrypto("decrypt", sql,
142                                                                                                        password, args,
143                                                                                                        callback);
144                                return null; // decrypted results will arrive asynchronously
145                        }
146
147                        // execute the SQL and get the results
148                        var rs = this.db.execute(sql, args);
149                       
150                        // Gears ResultSet object's are ugly -- normalize
151                        // these into something JavaScript programmers know
152                        // how to work with, basically an array of
153                        // JavaScript objects where each property name is
154                        // simply the field name for a column of data
155                        rs = this._normalizeResults(rs);
156               
157                        if(this._autoClose){
158                                this.close();
159                        }
160               
161                        return rs;
162                }catch(exp){
163                        exp = exp.message||exp;
164                       
165                        console.debug("SQL Exception: " + exp);
166                       
167                        if(this._autoClose){
168                                try{
169                                        this.close();
170                                }catch(e){
171                                        console.debug("Error closing database: "
172                                                                        + e.message||e);
173                                }
174                        }
175               
176                        throw exp;
177                }
178               
179                return null;
180        },
181
182        _initDb: function(){
183                if(!this.db){
184                        try{
185                                this.db = google.gears.factory.create('beta.database', '1.0');
186                        }catch(exp){
187                                dojo.setObject("google.gears.denied", true);
188                                if(dojox.off){
189                                  dojox.off.onFrameworkEvent("coreOperationFailed");
190                                }
191                                throw "Google Gears must be allowed to run";
192                        }
193                }
194        },
195
196        _printDebugSQL: function(sql, args){
197                var msg = "dojox.sql(\"" + sql + "\"";
198                for(var i = 0; i < args.length; i++){
199                        if(typeof args[i] == "string"){
200                                msg += ", \"" + args[i] + "\"";
201                        }else{
202                                msg += ", " + args[i];
203                        }
204                }
205                msg += ")";
206       
207                console.debug(msg);
208        },
209
210        _normalizeResults: function(rs){
211                var results = [];
212                if(!rs){ return []; }
213       
214                while(rs.isValidRow()){
215                        var row = {};
216               
217                        for(var i = 0; i < rs.fieldCount(); i++){
218                                var fieldName = rs.fieldName(i);
219                                var fieldValue = rs.field(i);
220                                row[fieldName] = fieldValue;
221                        }
222               
223                        results.push(row);
224               
225                        rs.next();
226                }
227       
228                rs.close();
229               
230                return results;
231        },
232
233        _needsEncrypt: function(sql){
234                return /encrypt\([^\)]*\)/i.test(sql);
235        },
236
237        _needsDecrypt: function(sql){
238                return /decrypt\([^\)]*\)/i.test(sql);
239        }
240});
241
242dojo.declare("dojox.sql._SQLCrypto", null, {
243        // summary:
244        //      A private class encapsulating any cryptography that must be done
245        //      on a SQL statement. We instantiate this class and have it hold
246        //      it's state so that we can potentially have several encryption
247        //      operations happening at the same time by different SQL statements.
248        constructor: function(action, sql, password, args, callback){
249                if(action == "encrypt"){
250                        this._execEncryptSQL(sql, password, args, callback);
251                }else{
252                        this._execDecryptSQL(sql, password, args, callback);
253                }
254        },
255       
256        _execEncryptSQL: function(sql, password, args, callback){
257                // strip the ENCRYPT/DECRYPT keywords from the SQL
258                var strippedSQL = this._stripCryptoSQL(sql);
259       
260                // determine what arguments need encryption
261                var encryptColumns = this._flagEncryptedArgs(sql, args);
262       
263                // asynchronously encrypt each argument that needs it
264                var self = this;
265                this._encrypt(strippedSQL, password, args, encryptColumns, function(finalArgs){
266                        // execute the SQL
267                        var error = false;
268                        var resultSet = [];
269                        var exp = null;
270                        try{
271                                resultSet = dojox.sql.db.execute(strippedSQL, finalArgs);
272                        }catch(execError){
273                                error = true;
274                                exp = execError.message||execError;
275                        }
276               
277                        // was there an error during SQL execution?
278                        if(exp != null){
279                                if(dojox.sql._autoClose){
280                                        try{ dojox.sql.close(); }catch(e){}
281                                }
282                       
283                                callback(null, true, exp.toString());
284                                return;
285                        }
286               
287                        // normalize SQL results into a JavaScript object
288                        // we can work with
289                        resultSet = dojox.sql._normalizeResults(resultSet);
290               
291                        if(dojox.sql._autoClose){
292                                dojox.sql.close();
293                        }
294                               
295                        // are any decryptions necessary on the result set?
296                        if(dojox.sql._needsDecrypt(sql)){
297                                // determine which of the result set columns needs decryption
298                                var needsDecrypt = self._determineDecryptedColumns(sql);
299
300                                // now decrypt columns asynchronously
301                                // decrypt columns that need it
302                                self._decrypt(resultSet, needsDecrypt, password, function(finalResultSet){
303                                        callback(finalResultSet, false, null);
304                                });
305                        }else{
306                                callback(resultSet, false, null);
307                        }
308                });
309        },
310
311        _execDecryptSQL: function(sql, password, args, callback){
312                // strip the ENCRYPT/DECRYPT keywords from the SQL
313                var strippedSQL = this._stripCryptoSQL(sql);
314       
315                // determine which columns needs decryption; this either
316                // returns the value *, which means all result set columns will
317                // be decrypted, or it will return the column names that need
318                // decryption set on a hashtable so we can quickly test a given
319                // column name; the key is the column name that needs
320                // decryption and the value is 'true' (i.e. needsDecrypt["someColumn"]
321                // would return 'true' if it needs decryption, and would be 'undefined'
322                // or false otherwise)
323                var needsDecrypt = this._determineDecryptedColumns(sql);
324       
325                // execute the SQL
326                var error = false;
327                var resultSet = [];
328                var exp = null;
329                try{
330                        resultSet = dojox.sql.db.execute(strippedSQL, args);
331                }catch(execError){
332                        error = true;
333                        exp = execError.message||execError;
334                }
335       
336                // was there an error during SQL execution?
337                if(exp != null){
338                        if(dojox.sql._autoClose){
339                                try{ dojox.sql.close(); }catch(e){}
340                        }
341               
342                        callback(resultSet, true, exp.toString());
343                        return;
344                }
345       
346                // normalize SQL results into a JavaScript object
347                // we can work with
348                resultSet = dojox.sql._normalizeResults(resultSet);
349       
350                if(dojox.sql._autoClose){
351                        dojox.sql.close();
352                }
353       
354                // decrypt columns that need it
355                this._decrypt(resultSet, needsDecrypt, password, function(finalResultSet){
356                        callback(finalResultSet, false, null);
357                });
358        },
359
360        _encrypt: function(sql, password, args, encryptColumns, callback){
361                //console.debug("_encrypt, sql="+sql+", password="+password+", encryptColumns="+encryptColumns+", args="+args);
362       
363                this._totalCrypto = 0;
364                this._finishedCrypto = 0;
365                this._finishedSpawningCrypto = false;
366                this._finalArgs = args;
367       
368                for(var i = 0; i < args.length; i++){
369                        if(encryptColumns[i]){
370                                // we have an encrypt() keyword -- get just the value inside
371                                // the encrypt() parantheses -- for now this must be a ?
372                                var sqlParam = args[i];
373                                var paramIndex = i;
374                       
375                                // update the total number of encryptions we know must be done asynchronously
376                                this._totalCrypto++;
377                       
378                                // FIXME: This currently uses DES as a proof-of-concept since the
379                                // DES code used is quite fast and was easy to work with. Modify dojox.sql
380                                // to be able to specify a different encryption provider through a
381                                // a SQL-like syntax, such as dojox.sql("SET ENCRYPTION BLOWFISH"),
382                                // and modify the dojox.crypto.Blowfish code to be able to work using
383                                // a Google Gears Worker Pool
384                       
385                                // do the actual encryption now, asychronously on a Gears worker thread
386                                dojox.sql._crypto.encrypt(sqlParam, password, dojo.hitch(this, function(results){
387                                        // set the new encrypted value
388                                        this._finalArgs[paramIndex] = results;
389                                        this._finishedCrypto++;
390                                        // are we done with all encryption?
391                                        if(this._finishedCrypto >= this._totalCrypto
392                                                && this._finishedSpawningCrypto){
393                                                callback(this._finalArgs);
394                                        }
395                                }));
396                        }
397                }
398       
399                this._finishedSpawningCrypto = true;
400        },
401
402        _decrypt: function(resultSet, needsDecrypt, password, callback){
403                //console.debug("decrypt, resultSet="+resultSet+", needsDecrypt="+needsDecrypt+", password="+password);
404               
405                this._totalCrypto = 0;
406                this._finishedCrypto = 0;
407                this._finishedSpawningCrypto = false;
408                this._finalResultSet = resultSet;
409       
410                for(var i = 0; i < resultSet.length; i++){
411                        var row = resultSet[i];
412               
413                        // go through each of the column names in row,
414                        // seeing if they need decryption
415                        for(var columnName in row){
416                                if(needsDecrypt == "*" || needsDecrypt[columnName]){
417                                        this._totalCrypto++;
418                                        var columnValue = row[columnName];
419                               
420                                        // forming a closure here can cause issues, with values not cleanly
421                                        // saved on Firefox/Mac OS X for some of the values above that
422                                        // are needed in the callback below; call a subroutine that will form
423                                        // a closure inside of itself instead
424                                        this._decryptSingleColumn(columnName, columnValue, password, i,
425                                                                                                function(finalResultSet){
426                                                callback(finalResultSet);
427                                        });
428                                }
429                        }
430                }
431       
432                this._finishedSpawningCrypto = true;
433        },
434
435        _stripCryptoSQL: function(sql){
436                // replace all DECRYPT(*) occurrences with a *
437                sql = sql.replace(/DECRYPT\(\*\)/ig, "*");
438       
439                // match any ENCRYPT(?, ?, ?, etc) occurrences,
440                // then replace with just the question marks in the
441                // middle
442                var matches = sql.match(/ENCRYPT\([^\)]*\)/ig);
443                if(matches != null){
444                        for(var i = 0; i < matches.length; i++){
445                                var encryptStatement = matches[i];
446                                var encryptValue = encryptStatement.match(/ENCRYPT\(([^\)]*)\)/i)[1];
447                                sql = sql.replace(encryptStatement, encryptValue);
448                        }
449                }
450       
451                // match any DECRYPT(COL1, COL2, etc) occurrences,
452                // then replace with just the column names
453                // in the middle
454                matches = sql.match(/DECRYPT\([^\)]*\)/ig);
455                if(matches != null){
456                        for(i = 0; i < matches.length; i++){
457                                var decryptStatement = matches[i];
458                                var decryptValue = decryptStatement.match(/DECRYPT\(([^\)]*)\)/i)[1];
459                                sql = sql.replace(decryptStatement, decryptValue);
460                        }
461                }
462       
463                return sql;
464        },
465
466        _flagEncryptedArgs: function(sql, args){
467                // capture literal strings that have question marks in them,
468                // and also capture question marks that stand alone
469                var tester = new RegExp(/([\"][^\"]*\?[^\"]*[\"])|([\'][^\']*\?[^\']*[\'])|(\?)/ig);
470                var matches;
471                var currentParam = 0;
472                var results = [];
473                while((matches = tester.exec(sql)) != null){
474                        var currentMatch = RegExp.lastMatch+"";
475
476                        // are we a literal string? then ignore it
477                        if(/^[\"\']/.test(currentMatch)){
478                                continue;
479                        }
480
481                        // do we have an encrypt keyword to our left?
482                        var needsEncrypt = false;
483                        if(/ENCRYPT\([^\)]*$/i.test(RegExp.leftContext)){
484                                needsEncrypt = true;
485                        }
486
487                        // set the encrypted flag
488                        results[currentParam] = needsEncrypt;
489
490                        currentParam++;
491                }
492       
493                return results;
494        },
495
496        _determineDecryptedColumns: function(sql){
497                var results = {};
498
499                if(/DECRYPT\(\*\)/i.test(sql)){
500                        results = "*";
501                }else{
502                        var tester = /DECRYPT\((?:\s*\w*\s*\,?)*\)/ig;
503                        var matches = tester.exec(sql);
504                        while(matches){
505                                var lastMatch = new String(RegExp.lastMatch);
506                                var columnNames = lastMatch.replace(/DECRYPT\(/i, "");
507                                columnNames = columnNames.replace(/\)/, "");
508                                columnNames = columnNames.split(/\s*,\s*/);
509                                dojo.forEach(columnNames, function(column){
510                                        if(/\s*\w* AS (\w*)/i.test(column)){
511                                                column = column.match(/\s*\w* AS (\w*)/i)[1];
512                                        }
513                                        results[column] = true;
514                                });
515                               
516                                matches = tester.exec(sql)
517                        }
518                }
519
520                return results;
521        },
522
523        _decryptSingleColumn: function(columnName, columnValue, password, currentRowIndex,
524                                                                                        callback){
525                //console.debug("decryptSingleColumn, columnName="+columnName+", columnValue="+columnValue+", currentRowIndex="+currentRowIndex)
526                dojox.sql._crypto.decrypt(columnValue, password, dojo.hitch(this, function(results){
527                        // set the new decrypted value
528                        this._finalResultSet[currentRowIndex][columnName] = results;
529                        this._finishedCrypto++;
530                       
531                        // are we done with all encryption?
532                        if(this._finishedCrypto >= this._totalCrypto
533                                && this._finishedSpawningCrypto){
534                                //console.debug("done with all decrypts");
535                                callback(this._finalResultSet);
536                        }
537                }));
538        }
539});
540
541(function(){
542
543        var orig_sql = dojox.sql;
544        dojox.sql = new Function("return dojox.sql._exec(arguments);");
545        dojo.mixin(dojox.sql, orig_sql);
546       
547})();
Note: See TracBrowser for help on using the repository browser.