1 | dojo.provide("dojox.sql._base"); |
---|
2 | dojo.require("dojox.sql._crypto"); |
---|
3 | |
---|
4 | dojo.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 | |
---|
242 | dojo.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 | })(); |
---|