// ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2006-2011 Strobe Inc. and contributors. // Portions ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ========================================================================== /*globals ie7userdata openDatabase*/ /**

@class

The UserDefaults object provides an easy way to store user preferences in
your application on the local machine.  You use this by providing built-in
defaults using the SC.userDefaults.defaults() method.  You can also
implement the UserDefaultsDelegate interface to be notified whenever a
default is required.

You should also set the userDomain property on the defaults on page load.
This will allow the UserDefaults application to store/fetch keys from
localStorage for the correct user.

You can also set an appDomain property if you want.  This will be
automatically prepended to key names with no slashes in them.

SC.userDefaults.getPath("global:contactInfo.userName");

@extends SC.Object
@since SproutCore 1.0

*/ SC.UserDefaults = SC.Object.extend(/** @scope SC.UserDefaults.prototype */ {

ready: NO,

/**
  the default domain for the user.  This will be used to store keys in
  local storage.  If you do not set this property, the wrong values may be
  returned.
*/
userDomain: null,

/**
  The default app domain for the user.  Any keys that do not include a
  slash will be prefixed with this app domain key when getting/setting.
*/
appDomain: null,

/** @private
  Defaults.  These will be used if not defined on localStorage.
*/
_defaults: null,

_safari3DB: null,

/**
  Invoke this method to set the builtin defaults.  This will cause all
  properties to change.
*/
defaults: function(newDefaults) {
  this._defaults = newDefaults ;
  this.allPropertiesDidChange();
},

/**
  Attempts to read a user default from local storage.  If not found on
  localStorage, use the the local defaults, if defined.  If the key passed
  does not include a slash, then add the appDomain or use "app/".

  @param {String} keyName
  @returns {Object} read value
*/
readDefault: function(keyName) {
  // Note: different implementations of localStorage may return 'null' or
  // may return 'undefined' for missing properties so use SC.none() to check
  // for the existence of ret throughout this function.
  var isIE7, ret, userKeyName, localStorage, key, del, storageSafari3;

  // namespace keyname
  keyName = this._normalizeKeyName(keyName);
  userKeyName = this._userKeyName(keyName);

  // look into recently written values
  if (this._written) { ret = this._written[userKeyName]; }

  // attempt to read from localStorage
  isIE7 = SC.browser.isIE &&
      SC.browser.compare(SC.browser.version, '7') === 0;

  if(isIE7) {
    localStorage=document.body;
    try{
      localStorage.load("SC.UserDefaults");
    }catch(e){
      SC.Logger.error("Couldn't load userDefaults in IE7: "+e.description);
    }
  }else if(this.HTML5DB_noLocalStorage){
    storageSafari3 = this._safari3DB;
  }else{
    localStorage = window.localStorage ;
    if (!localStorage && window.globalStorage) {
      localStorage = window.globalStorage[window.location.hostname];
    }
  }
  if (localStorage || storageSafari3) {
    key=["SC.UserDefaults",userKeyName].join('-at-');
    if(isIE7) {
      ret=localStorage.getAttribute(key.replace(/\W/gi, ''));
    } else if(storageSafari3) {
      ret = this.dataHash[key];
    } else {
      ret = localStorage[key];
    }
    if (!SC.none(ret)) {
      try { ret = SC.json.decode(ret); }
      catch(ex) {}
    }
  }

  // if not found in localStorage, try to notify delegate
  del = this.delegate ;
  if (del && del.userDefaultsNeedsDefault) {
    ret = del.userDefaultsNeedsDefault(this, keyName, userKeyName);
  }

  // if not found in localStorage or delegate, try to find in defaults
  if (SC.none(ret) && this._defaults) {
    ret = this._defaults[userKeyName] || this._defaults[keyName];
  }

  return ret ;
},

/**
  Attempts to write the user default to local storage or at least saves them
  for now.  Also notifies that the value has changed.

  @param {String} keyName
  @param {Object} value
  @returns {SC.UserDefault} receiver
*/
writeDefault: function(keyName, value) {
  var isIE7, userKeyName, written, localStorage, key, del, storageSafari3;

  keyName = this._normalizeKeyName(keyName);
  userKeyName = this._userKeyName(keyName);

  // save to local hash
  written = this._written ;
  if (!written) { written = this._written = {}; }
  written[userKeyName] = value ;

  // save to local storage
  isIE7 = SC.browser.isIE &&
      SC.browser.compare(SC.browser.version, '7') === 0;

  if(isIE7){
    localStorage=document.body;
  }else if(this.HTML5DB_noLocalStorage){
    storageSafari3 = this._safari3DB;
  }else{
     localStorage = window.localStorage ;
     if (!localStorage && window.globalStorage) {
       localStorage = window.globalStorage[window.location.hostname];
     }
  }
  key=["SC.UserDefaults",userKeyName].join('-at-');
  if (localStorage || storageSafari3) {
    var encodedValue = SC.json.encode(value);
    if(isIE7){
      localStorage.setAttribute(key.replace(/\W/gi, ''), encodedValue);
      localStorage.save("SC.UserDefaults");
    }else if(storageSafari3){
      var obj = this;
      storageSafari3.transaction(
        function (t) {
          t.executeSql("delete from SCLocalStorage where key = ?", [key],
            function (){
              t.executeSql("insert into SCLocalStorage(key, value)"+
                          " VALUES ('"+key+"', '"+encodedValue+"');",
                          [], obj._nullDataHandler, obj.killTransaction
              );
            }
          );
        }
      );
      this.dataHash[key] = encodedValue;
    }else{
      try{
        localStorage[key] = encodedValue;
      }catch(e){
        SC.Logger.error("Failed using localStorage. "+e);
      }
    }
  }

  // also notify delegate
  del = this.delegate;
  if (del && del.userDefaultsDidChange) {
    del.userDefaultsDidChange(this, keyName, value, userKeyName);
  }

  return this ;
},

/**
  Removed the passed keyName from the written hash and local storage.

  @param {String} keyName
  @returns {SC.UserDefaults} receiver
*/
resetDefault: function(keyName) {
  var fullKeyName, isIE7, userKeyName, written, localStorage, key, storageSafari3;
  fullKeyName = this._normalizeKeyName(keyName);
  userKeyName = this._userKeyName(fullKeyName);

  this.propertyWillChange(keyName);
  this.propertyWillChange(fullKeyName);

  written = this._written;
  if (written) delete written[userKeyName];

  isIE7 = SC.browser.isIE &&
      SC.browser.compare(SC.browser.version, '7') === 0;

  if(isIE7){
     localStorage=document.body;
  }else if(this.HTML5DB_noLocalStorage){
       storageSafari3 = this._safari3DB;
  }else{
     localStorage = window.localStorage ;
     if (!localStorage && window.globalStorage) {
       localStorage = window.globalStorage[window.location.hostname];
     }
  }

  key=["SC.UserDefaults",userKeyName].join('-at-');

  if (localStorage) {
    if(isIE7){
      localStorage.setAttribute(key.replace(/\W/gi, ''), null);
      localStorage.save("SC.UserDefaults");
    } else if(storageSafari3){
      var obj = this;
      storageSafari3.transaction(
        function (t) {
          t.executeSql("delete from SCLocalStorage where key = ?", [key], null);
        }
      );
      delete this.dataHash[key];
    }else{
      // In case error occurs while deleting local storage in any browser,
      // do not allow it to propagate further
      try{
        delete localStorage[key];
      } catch(e) {
        SC.Logger.warn('Deleting local storage encountered a problem. '+e);
      }
    }
  }

  this.propertyDidChange(keyName);
  this.propertyDidChange(fullKeyName);
  return this ;
},

/**
  Is called whenever you .get() or .set() values on this object

  @param {Object} key
  @param {Object} value
  @returns {Object}
*/
unknownProperty: function(key, value) {
  if (value === undefined) {
    return this.readDefault(key) ;
  } else {
    this.writeDefault(key, value);
    return value ;
  }
},

/**
  Normalize the passed key name.  Used by all accessors to automatically
  insert an appName if needed.
*/
_normalizeKeyName: function(keyName) {
  if (keyName.indexOf(':')<0) {
    var domain = this.get('appDomain') || 'app';
    keyName = [domain, keyName].join(':');
  }
  return keyName;
},

/**
  Builds a user key name from the passed key name
*/
_userKeyName: function(keyName) {
  var user = this.get('userDomain') || '(anonymous)' ;
  return [user,keyName].join('-at-');
},

_domainDidChange: function() {
  var didChange = NO;
  if (this.get("userDomain") !== this._scud_userDomain) {
    this._scud_userDomain = this.get('userDomain');
    didChange = YES;
  }

  if (this.get('appDomain') !== this._scud_appDomain) {
    this._scud_appDomain = this.get('appDomain');
    didChange = YES;
  }

  if (didChange) this.allPropertiesDidChange();
}.observes('userDomain', 'appDomain'),

init: function() {
  sc_super();
  var isIE7;

  // Increment the jQuery ready counter, so that SproutCore will
  // defer loading the app until the user defaults are available.
  jQuery.readyWait++;

  if(SC.userDefaults && SC.userDefaults.get('dataHash')){
    var dh = SC.userDefaults.get('dataHash');
    if (dh) this.dataHash=SC.userDefaults.get('dataHash');
  }
  this._scud_userDomain = this.get('userDomain');
  this._scud_appDomain  = this.get('appDomain');

  isIE7 = SC.browser.isIE &&
      SC.browser.compare(SC.browser.version, '7') === 0;

  if(isIE7){
    //Add user behavior userData. This works in all versions of IE.
    //Adding to the body as is the only element never removed.
    document.body.addBehavior('#default#userData');
  }
  this.HTML5DB_noLocalStorage = SC.browser.isWebkit &&
    SC.browser.compare(SC.browser.engineVersion, '523')>0 &&
    SC.browser.compare(SC.browser.engineVersion, '528')<0;
  if(this.HTML5DB_noLocalStorage){
    var myDB;
    try {
      if (!window.openDatabase) {
        SC.Logger.error("Trying to load a database with safari version 3.1 "+
                "to get SC.UserDefaults to work. You are either in a"+
                " previous version or there is a problem with your browser.");
        return;
      } else {
        var shortName = 'scdb',
            version = '1.0',
            displayName = 'SproutCore database',
            maxSize = 65536; // in bytes,
        myDB = openDatabase(shortName, version, displayName, maxSize);

        // You should have a database instance in myDB.

      }
    } catch(e) {
      SC.Logger.error("Trying to load a database with safari version 3.1 "+
              "to get SC.UserDefaults to work. You are either in a"+
              " previous version or there is a problem with your browser.");
      return;
    }

    if(myDB){
      var obj = this;
      myDB.transaction(
        function (transaction) {
          transaction.executeSql('CREATE TABLE IF NOT EXISTS SCLocalStorage'+
            '(key TEXT NOT NULL PRIMARY KEY, value TEXT NOT NULL);',
            [], obj._nullDataHandler, obj.killTransaction);
        }
      );
      myDB.transaction(
        function (transaction) {

          transaction.parent = obj;
          transaction.executeSql('SELECT * from SCLocalStorage;',
              [], function(transaction, results){
                var hash={}, row;
                for(var i=0, iLen=results.rows.length; i<iLen; i++){
                  row=results.rows.item(i);
                  hash[row['key']]=row['value'];
                }
                transaction.parent.dataHash = hash;
                SC.run(function() { jQuery.ready(true); });
              }, obj.killTransaction);
        }
      );
      this._safari3DB=myDB;
    }
  }else{
    jQuery.ready(true);
  }
},

//Private methods to use if user defaults uses the database in safari 3
_killTransaction: function(transaction, error){
  return true; // fatal transaction error
},

_nullDataHandler: function(transaction, results){}

});

/** global user defaults. */ SC.userDefaults = SC.UserDefaults.create();