1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 /*globals ie7userdata openDatabase*/
  8 /**
  9   @class
 10 
 11   The UserDefaults object provides an easy way to store user preferences in
 12   your application on the local machine.  You use this by providing built-in
 13   defaults using the SC.userDefaults.defaults() method.  You can also
 14   implement the UserDefaultsDelegate interface to be notified whenever a
 15   default is required.
 16 
 17   You should also set the userDomain property on the defaults on page load.
 18   This will allow the UserDefaults application to store/fetch keys from
 19   localStorage for the correct user.
 20 
 21   You can also set an appDomain property if you want.  This will be
 22   automatically prepended to key names with no slashes in them.
 23 
 24   SC.userDefaults.getPath("global:contactInfo.userName");
 25 
 26   @extends SC.Object
 27   @since SproutCore 1.0
 28 */
 29 SC.UserDefaults = SC.Object.extend(/** @scope SC.UserDefaults.prototype */ {
 30 
 31   ready: NO,
 32 
 33   /**
 34     the default domain for the user.  This will be used to store keys in
 35     local storage.  If you do not set this property, the wrong values may be
 36     returned.
 37   */
 38   userDomain: null,
 39 
 40   /**
 41     The default app domain for the user.  Any keys that do not include a
 42     slash will be prefixed with this app domain key when getting/setting.
 43   */
 44   appDomain: null,
 45 
 46   /** @private
 47     Defaults.  These will be used if not defined on localStorage.
 48   */
 49   _defaults: null,
 50 
 51   _safari3DB: null,
 52 
 53   /**
 54     Invoke this method to set the builtin defaults.  This will cause all
 55     properties to change.
 56   */
 57   defaults: function(newDefaults) {
 58     this._defaults = newDefaults ;
 59     this.allPropertiesDidChange();
 60   },
 61 
 62   /**
 63     Attempts to read a user default from local storage.  If not found on
 64     localStorage, use the the local defaults, if defined.  If the key passed
 65     does not include a slash, then add the appDomain or use "app/".
 66 
 67     @param {String} keyName
 68     @returns {Object} read value
 69   */
 70   readDefault: function(keyName) {
 71     // Note: different implementations of localStorage may return 'null' or
 72     // may return 'undefined' for missing properties so use SC.none() to check
 73     // for the existence of ret throughout this function.
 74     var isIE7, ret, userKeyName, localStorage, key, del, storageSafari3;
 75 
 76     // namespace keyname
 77     keyName = this._normalizeKeyName(keyName);
 78     userKeyName = this._userKeyName(keyName);
 79 
 80     // look into recently written values
 81     if (this._written) { ret = this._written[userKeyName]; }
 82 
 83     // attempt to read from localStorage
 84     isIE7 = SC.browser.isIE &&
 85         SC.browser.compare(SC.browser.version, '7') === 0;
 86 
 87     if(isIE7) {
 88       localStorage=document.body;
 89       try{
 90         localStorage.load("SC.UserDefaults");
 91       }catch(e){
 92         SC.Logger.error("Couldn't load userDefaults in IE7: "+e.description);
 93       }
 94     }else if(this.HTML5DB_noLocalStorage){
 95       storageSafari3 = this._safari3DB;
 96     }else{
 97       localStorage = window.localStorage ;
 98       if (!localStorage && window.globalStorage) {
 99         localStorage = window.globalStorage[window.location.hostname];
100       }
101     }
102     if (localStorage || storageSafari3) {
103       key=["SC.UserDefaults",userKeyName].join('-at-');
104       if(isIE7) {
105         ret=localStorage.getAttribute(key.replace(/\W/gi, ''));
106       } else if(storageSafari3) {
107         ret = this.dataHash[key];
108       } else {
109         ret = localStorage[key];
110       }
111       if (!SC.none(ret)) {
112         try { ret = SC.json.decode(ret); }
113         catch(ex) {}
114       }
115     }
116 
117     // if not found in localStorage, try to notify delegate
118     del = this.delegate ;
119     if (del && del.userDefaultsNeedsDefault) {
120       ret = del.userDefaultsNeedsDefault(this, keyName, userKeyName);
121     }
122 
123     // if not found in localStorage or delegate, try to find in defaults
124     if (SC.none(ret) && this._defaults) {
125       ret = this._defaults[userKeyName] || this._defaults[keyName];
126     }
127 
128     return ret ;
129   },
130 
131   /**
132     Attempts to write the user default to local storage or at least saves them
133     for now.  Also notifies that the value has changed.
134 
135     @param {String} keyName
136     @param {Object} value
137     @returns {SC.UserDefault} receiver
138   */
139   writeDefault: function(keyName, value) {
140     var isIE7, userKeyName, written, localStorage, key, del, storageSafari3;
141 
142     keyName = this._normalizeKeyName(keyName);
143     userKeyName = this._userKeyName(keyName);
144 
145     // save to local hash
146     written = this._written ;
147     if (!written) { written = this._written = {}; }
148     written[userKeyName] = value ;
149 
150     // save to local storage
151     isIE7 = SC.browser.isIE &&
152         SC.browser.compare(SC.browser.version, '7') === 0;
153 
154     if(isIE7){
155       localStorage=document.body;
156     }else if(this.HTML5DB_noLocalStorage){
157       storageSafari3 = this._safari3DB;
158     }else{
159        localStorage = window.localStorage ;
160        if (!localStorage && window.globalStorage) {
161          localStorage = window.globalStorage[window.location.hostname];
162        }
163     }
164     key=["SC.UserDefaults",userKeyName].join('-at-');
165     if (localStorage || storageSafari3) {
166       var encodedValue = SC.json.encode(value);
167       if(isIE7){
168         localStorage.setAttribute(key.replace(/\W/gi, ''), encodedValue);
169         localStorage.save("SC.UserDefaults");
170       }else if(storageSafari3){
171         var obj = this;
172         storageSafari3.transaction(
173           function (t) {
174             t.executeSql("delete from SCLocalStorage where key = ?", [key],
175               function (){
176                 t.executeSql("insert into SCLocalStorage(key, value)"+
177                             " VALUES ('"+key+"', '"+encodedValue+"');",
178                             [], obj._nullDataHandler, obj.killTransaction
179                 );
180               }
181             );
182           }
183         );
184         this.dataHash[key] = encodedValue;
185       }else{
186         try{
187           localStorage[key] = encodedValue;
188         }catch(e){
189           SC.Logger.error("Failed using localStorage. "+e);
190         }
191       }
192     }
193 
194     // also notify delegate
195     del = this.delegate;
196     if (del && del.userDefaultsDidChange) {
197       del.userDefaultsDidChange(this, keyName, value, userKeyName);
198     }
199 
200     return this ;
201   },
202 
203   /**
204     Removed the passed keyName from the written hash and local storage.
205 
206     @param {String} keyName
207     @returns {SC.UserDefaults} receiver
208   */
209   resetDefault: function(keyName) {
210     var fullKeyName, isIE7, userKeyName, written, localStorage, key, storageSafari3;
211     fullKeyName = this._normalizeKeyName(keyName);
212     userKeyName = this._userKeyName(fullKeyName);
213 
214     this.propertyWillChange(keyName);
215     this.propertyWillChange(fullKeyName);
216 
217     written = this._written;
218     if (written) delete written[userKeyName];
219 
220     isIE7 = SC.browser.isIE &&
221         SC.browser.compare(SC.browser.version, '7') === 0;
222 
223     if(isIE7){
224        localStorage=document.body;
225     }else if(this.HTML5DB_noLocalStorage){
226          storageSafari3 = this._safari3DB;
227     }else{
228        localStorage = window.localStorage ;
229        if (!localStorage && window.globalStorage) {
230          localStorage = window.globalStorage[window.location.hostname];
231        }
232     }
233 
234     key=["SC.UserDefaults",userKeyName].join('-at-');
235 
236     if (localStorage) {
237       if(isIE7){
238         localStorage.setAttribute(key.replace(/\W/gi, ''), null);
239         localStorage.save("SC.UserDefaults");
240       } else if(storageSafari3){
241         var obj = this;
242         storageSafari3.transaction(
243           function (t) {
244             t.executeSql("delete from SCLocalStorage where key = ?", [key], null);
245           }
246         );
247         delete this.dataHash[key];
248       }else{
249         // In case error occurs while deleting local storage in any browser,
250         // do not allow it to propagate further
251         try{
252           delete localStorage[key];
253         } catch(e) {
254           SC.Logger.warn('Deleting local storage encountered a problem. '+e);
255         }
256       }
257     }
258 
259 
260     this.propertyDidChange(keyName);
261     this.propertyDidChange(fullKeyName);
262     return this ;
263   },
264 
265   /**
266     Is called whenever you .get() or .set() values on this object
267 
268     @param {Object} key
269     @param {Object} value
270     @returns {Object}
271   */
272   unknownProperty: function(key, value) {
273     if (value === undefined) {
274       return this.readDefault(key) ;
275     } else {
276       this.writeDefault(key, value);
277       return value ;
278     }
279   },
280 
281   /**
282     Normalize the passed key name.  Used by all accessors to automatically
283     insert an appName if needed.
284   */
285   _normalizeKeyName: function(keyName) {
286     if (keyName.indexOf(':')<0) {
287       var domain = this.get('appDomain') || 'app';
288       keyName = [domain, keyName].join(':');
289     }
290     return keyName;
291   },
292 
293   /**
294     Builds a user key name from the passed key name
295   */
296   _userKeyName: function(keyName) {
297     var user = this.get('userDomain') || '(anonymous)' ;
298     return [user,keyName].join('-at-');
299   },
300 
301   _domainDidChange: function() {
302     var didChange = NO;
303     if (this.get("userDomain") !== this._scud_userDomain) {
304       this._scud_userDomain = this.get('userDomain');
305       didChange = YES;
306     }
307 
308     if (this.get('appDomain') !== this._scud_appDomain) {
309       this._scud_appDomain = this.get('appDomain');
310       didChange = YES;
311     }
312 
313     if (didChange) this.allPropertiesDidChange();
314   }.observes('userDomain', 'appDomain'),
315 
316   init: function() {
317     sc_super();
318     var isIE7;
319 
320     // Increment the jQuery ready counter, so that SproutCore will
321     // defer loading the app until the user defaults are available.
322     jQuery.readyWait++;
323 
324     if(SC.userDefaults && SC.userDefaults.get('dataHash')){
325       var dh = SC.userDefaults.get('dataHash');
326       if (dh) this.dataHash=SC.userDefaults.get('dataHash');
327     }
328     this._scud_userDomain = this.get('userDomain');
329     this._scud_appDomain  = this.get('appDomain');
330 
331     isIE7 = SC.browser.isIE &&
332         SC.browser.compare(SC.browser.version, '7') === 0;
333 
334     if(isIE7){
335       //Add user behavior userData. This works in all versions of IE.
336       //Adding to the body as is the only element never removed.
337       document.body.addBehavior('#default#userData');
338     }
339     this.HTML5DB_noLocalStorage = SC.browser.isWebkit &&
340       SC.browser.compare(SC.browser.engineVersion, '523')>0 &&
341       SC.browser.compare(SC.browser.engineVersion, '528')<0;
342     if(this.HTML5DB_noLocalStorage){
343       var myDB;
344       try {
345         if (!window.openDatabase) {
346           SC.Logger.error("Trying to load a database with safari version 3.1 "+
347                   "to get SC.UserDefaults to work. You are either in a"+
348                   " previous version or there is a problem with your browser.");
349           return;
350         } else {
351           var shortName = 'scdb',
352               version = '1.0',
353               displayName = 'SproutCore database',
354               maxSize = 65536; // in bytes,
355           myDB = openDatabase(shortName, version, displayName, maxSize);
356 
357           // You should have a database instance in myDB.
358 
359         }
360       } catch(e) {
361         SC.Logger.error("Trying to load a database with safari version 3.1 "+
362                 "to get SC.UserDefaults to work. You are either in a"+
363                 " previous version or there is a problem with your browser.");
364         return;
365       }
366 
367       if(myDB){
368         var obj = this;
369         myDB.transaction(
370           function (transaction) {
371             transaction.executeSql('CREATE TABLE IF NOT EXISTS SCLocalStorage'+
372               '(key TEXT NOT NULL PRIMARY KEY, value TEXT NOT NULL);',
373               [], obj._nullDataHandler, obj.killTransaction);
374           }
375         );
376         myDB.transaction(
377           function (transaction) {
378 
379             transaction.parent = obj;
380             transaction.executeSql('SELECT * from SCLocalStorage;',
381                 [], function(transaction, results){
382                   var hash={}, row;
383                   for(var i=0, iLen=results.rows.length; i<iLen; i++){
384                     row=results.rows.item(i);
385                     hash[row['key']]=row['value'];
386                   }
387                   transaction.parent.dataHash = hash;
388                   SC.run(function() { jQuery.ready(true); });
389                 }, obj.killTransaction);
390           }
391         );
392         this._safari3DB=myDB;
393       }
394     }else{
395       jQuery.ready(true);
396     }
397   },
398 
399 
400   //Private methods to use if user defaults uses the database in safari 3
401   _killTransaction: function(transaction, error){
402     return true; // fatal transaction error
403   },
404 
405   _nullDataHandler: function(transaction, results){}
406 });
407 
408 /** global user defaults. */
409 SC.userDefaults = SC.UserDefaults.create();
410