public
Last active

Storage polyfill

  • Download Gist
gistfile1.js
JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
if (typeof window.localStorage == 'undefined' || typeof window.sessionStorage == 'undefined') (function () {
 
var Storage = function (type) {
function createCookie(name, value, days) {
var date, expires;
 
if (days) {
date = new Date();
date.setTime(date.getTime()+(days*24*60*60*1000));
expires = "; expires="+date.toGMTString();
} else {
expires = "";
}
document.cookie = name+"="+value+expires+"; path=/";
}
 
function readCookie(name) {
var nameEQ = name + "=",
ca = document.cookie.split(';'),
i, c;
 
for (i=0; i < ca.length; i++) {
c = ca[i];
while (c.charAt(0)==' ') {
c = c.substring(1,c.length);
}
 
if (c.indexOf(nameEQ) == 0) {
return c.substring(nameEQ.length,c.length);
}
}
return null;
}
function setData(data) {
data = JSON.stringify(data);
if (type == 'session') {
window.name = data;
} else {
createCookie('localStorage', data, 365);
}
}
function clearData() {
if (type == 'session') {
window.name = '';
} else {
createCookie('localStorage', '', 365);
}
}
function getData() {
var data = type == 'session' ? window.name : readCookie('localStorage');
return data ? JSON.parse(data) : {};
}
 
 
// initialise if there's already data
var data = getData();
 
return {
length: 0,
clear: function () {
data = {};
this.length = 0;
clearData();
},
getItem: function (key) {
return data[key] === undefined ? null : data[key];
},
key: function (i) {
// not perfect, but works
var ctr = 0;
for (var k in data) {
if (ctr == i) return k;
else ctr++;
}
return null;
},
removeItem: function (key) {
delete data[key];
this.length--;
setData(data);
},
setItem: function (key, value) {
data[key] = value+''; // forces the value to a string
this.length++;
setData(data);
}
};
};
 
if (typeof window.localStorage == 'undefined') window.localStorage = new Storage('local');
if (typeof window.sessionStorage == 'undefined') window.sessionStorage = new Storage('session');
 
})();

Hi,

I'm using this nice script as a basis for my webshims lib. I found some bugs/possibilities to improve:

  1. the script is testing for fals-y values and returns null. return data[key] || null; you should test for the empty string and return this. there are x possibilities to do this, so i don't suggest a specific solution
  2. window.top can throw an error (cross-origin-policy). I currently test for access to several windows (in this order: opener, top, parent and then current window)
  3. although really minor issue you can't use ; in the key-string for cookie-based localStorage. this is possible in native localStorage and sessionStorage. I'm currently throwing an error with a better error-message, if the developer trys to set such an item.

regards
alex

Alex - all good feedback. I'm fixed point 1+2 - but not 3 (just because it needs a little more thinking - which I don't have this second!).

This looks really good, but what about the 'length' property?

This is essentially just giving cookies the local/sessionStorage API, right? So as JasonHanley asks, aren’t we going to bump into the length limit of cookies? At this point the only difference from just using cookies (besides the API) is that modern browsers are saved from the overhead of sending the data back on each HTTP request, right?

Might it be a better idea to instead use a secure server connection to a possibly-cacheable resource to represent local data? Then the polyfill could actually be using a traditional session architecture (cookie contains only a key used for associating the visitor with data stored on the server) instead of a cookie for data, solving the length problem. Other approaches would include using Flash cookies.

Thoughts?

@JasonHanley - doh - yes, I've missed that one. I'll update.

@alahogan - We're talking about IE7 and IE6 /only/ - because all other browsers support localStorage and sessionStorage. Note that the limit of cookies only affects localStorage, since sessionStorage is being stored in window.name which has around 2mb limit. The idea of deferring this to the server side is completely wrong IMHO, since we'd be moving client side tech to the server - and the whole idea of client storage is that it's, well, stored in the client!

Because cookies in IE7 and below are limited to 4,096 bytes, one solution would be to chunk the data to span across multiple cookies. I'd personally need a decent case for doing this and (again, personally) I've not come across this in my client projects as yet. I totally put my hands up and agree it's an issue, but according to Stats Counter, IE6 & IE7 are currently accounting for 15% of the market - that's low enough that it doesn't justify the work: http://gs.statcounter.com/#browser_version-ww-monthly-201001-201101 - of course, if you disagree, fork this script and let me know how you get on :)

I'd like to point out one non-IE use case: Fluid.app clears localStorage when the SSB is closed, which makes it useless for storing more persistent data like user preferences for a Userscript. I've been using a modified version of this to get around that.

@alahogan and remy - You can simply use SharedObject of flash to work around the length and the data-sending isssues of cookies. Like already sayed above, I'm using exactly this script as a base for my polyfill. If flash is available, I'm using ShardObject. The nice thing here is that flash has a default of 100kb storage (~25x more than cookie), but let's the user to allow unrestricted storage space (You can detect, if the storage is exceeded etc.). You can find a simple implementation with external interface @https://github.com/aFarkas/webshim/blob/master/swfs/localStorage/src/Main.as (It's 1.5kb of ActionScript 2.0, works in Flash 7+).

@remy - I think the length attribute (similiar to the keys-method and the IDL-access of storage-data) is neither very usefull nor reliable. (If a third-party script is using localStorage and a developer makes assumption on the length of the domStorage, his assumptions will fail. I can't think of a usecase.) Additionally, it will add a performance overhead on initialization.

This script does not work when trying to access pages from a file:// URL and instead gives a "Operation is not supported" code: 9 - Exception. This is fixed in YUI with this commit: http://yuilibrary.com/projects/yui3/ticket/2529165 - Maybe add that here too?

No - because WebStorage doesn't work offline because there's no origin to bind to. IIRC this matches the spec.

The sessionstorage-exception make it impossible to use localstorage with your snippet. Localstorage does work offline. That's the issue they've fixed.

@remy
He is somehow right. I have also changed the code from

if (typeof window.sessionStorage == 'undefined')

to:

if(!('sessionStorage' in window))

I've done more research, and localStorage does NOT work in IE and Chrome no matter what you do, only Firefox. Read some idea that all local apps could then access other app's content (Like things work on a regular harddrive, what a mess... ;) ).

That "fix" is simply a silent fail, so the underlying "issue" is not fixed, just ignored.

All storage doesn't work on file:/// because file is not an origin (domain). Not sure how Firefox can get around that apart from silent fail or undeclared permissions elevation. There's no way a web browser should be allowed to access contents of a hard drive outside the scope of the application (i.e. the cache). Even then there shouldn't be the remotest possibility of accessing data unrelated to the current domain.

@ShirtlessKirk: I think Firefox scopes the file access to only files in the same directory. Firefox 2 allowed access to any file, the sub-folder policy was new in Firefox 3. Other browsers allow no access at all.

@remy Your length should only start at zero if the data is empty to begin with. Also, you should only increment the length in setItem if the key doesn't already exist and only decrement the length in removeItem if the key does already exist.

Just for the sake of convention, shouldn't this script use the === operator as opposed to == ?

Ex/ if (typeof window.localStorage === 'undefined'

Just to let you know, there is a bug in Firefox, known but not fixed since 2009. A simple typeof window.localStorage causes an exception when executed locally. Please comment and vote at Bugzilla.

Just a heads up for anyone running into IE7 "JSON undefined" issue after inserting this polyfill. Grab Crockford's JSON polyfill (https://github.com/douglascrockford/JSON-js/blob/master/json2.js) and insert before this one. Nothing like a polyfill to help a polyfill. Hmmm... PP fixes IE. Something poetic there.

Also note you won't be able to use local storage shorthand with this (localStorage.mydata = 'whatever' ... localStorage.mydata).

Hi Remy - This looks really useful. What licence is it released under?

Many thanks!
Wills

Hey Remy, I just came across this and wondered if you thought about implementing this w/o cookies... possibly with globalStorage and userData like this: https://github.com/wojodesign/local-storage-js

@brettwejrowski - I'm inclined to totally agree.

@remy, With cookies disabled, merely accessing window.localStorage will throw a Security exception in Firefox (tested FF15). I'm having to add a try/catch in my app to fail gracefully.

In safari with private browsing, using local and session storage gives you:

Error: QUOTA_EXCEEDED_ERR: DOM Exception 22.

Would be nice with a try/catch at the top, so we also can use this in safari with private browsing. :-)

It seems that Opera Mini does not allow double quotes in cookies, that's why the polyfill is not working on it.
http://www.iandevlin.com/blog/2012/04/html5/cookies-json-localstorage-and-opera

I've made a simple fix with two replaces, that turn double quotes into single quotes, and back.
So far it seems to work everywhere, including Opera Mini.
https://gist.github.com/ghinda/6036998

Thanks for this. One comment. You've added JSON.parse and JSON.stringify in the getter and setter. I would pull that for a more accurate representation of the API. The native setters and getters require strings, so it might produce unexpected behavior if the polyfill glosses over this. In the setter, I'm okay with leaving it, since the native would not throw an error anyway, it just does a .toString and stores '[object Object]', but in the getter, you'd have to change your logic between the native and the polyfill.

So:

  function setData(data) {
    data = typeof data !== 'string' ? JSON.stringify(data) : data;
    if (type == 'session') {
      window.name = data;
    } else {
      createCookie('localStorage', data, 365);
    }
  }

  function getData() {
    var data = type == 'session' ? window.name : readCookie('localStorage');
    return data || {};
  }

Thoughts?

setItem: function (key, value) {
      data[key] = value+''; // forces the value to a string
      this.length++;
      setData(data);
    }

Shouldn't you check if the key was already set before incrementing length? Like:

setItem: function (key, value) {
      if (data[key] === undefined) this.length++;
      data[key] = value+''; // forces the value to a string
      setData(data);
    }

In Privacy mode in Safari window.localStorage exists while it's not operable. so I think function to check if operable needed:
var isStorageOperable = function (storage) {
try {
storage.setItem("storage", "");
storage.getItem("storage");
storage.removeItem("storage");
return true;
}
catch (err) {
return false;
}
};

and than add check for operable additionally to check for typeof undefined.

@AirRider3 @vadzappa I put your fixes in https://gist.github.com/Contra/6368485 - I also applied that length fix to remove (it wasn't checking if the key existed before changing the length)

@AirRider3 @vadzappa @Contra Even with that fix, I'm not sure that it works for private Safari. I think the override of

window.localStorage = new Storage('local');

doesn't do anything because window.localStorage is already implemented as far as private Safari is concerned, and allowing it to be overridden would be a security concern. Please correct me if I'm wrong - I'm still looking for a way to directly use window.localStorage/sessionStorage in private Safari without a data access layer. I tried running Contra's gist without success.

I think the only workaround for private browsing Safari requires you to overwrite the localStorage/sessionStorage prototypes, as overriding the object itself has no effect.

window.localStorage.__proto__ = new Storage('local');

Here's the complete gist with more complete localStorage support detection: https://gist.github.com/hasdavidc/8527456

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.