Merge "Allow wildcard searching in wiki IDs for interwiki user rights logs"
[lhc/web/wiklou.git] / resources / lib / jquery / jquery.jStorage.js
1 /*
2 * ----------------------------- JSTORAGE -------------------------------------
3 * Simple local storage wrapper to save data on the browser side, supporting
4 * all major browsers - IE6+, Firefox2+, Safari4+, Chrome4+ and Opera 10.5+
5 *
6 * Author: Andris Reinman, andris.reinman@gmail.com
7 * Project homepage: www.jstorage.info
8 *
9 * Licensed under Unlicense:
10 *
11 * This is free and unencumbered software released into the public domain.
12 *
13 * Anyone is free to copy, modify, publish, use, compile, sell, or
14 * distribute this software, either in source code form or as a compiled
15 * binary, for any purpose, commercial or non-commercial, and by any
16 * means.
17 *
18 * In jurisdictions that recognize copyright laws, the author or authors
19 * of this software dedicate any and all copyright interest in the
20 * software to the public domain. We make this dedication for the benefit
21 * of the public at large and to the detriment of our heirs and
22 * successors. We intend this dedication to be an overt act of
23 * relinquishment in perpetuity of all present and future rights to this
24 * software under copyright law.
25 *
26 * THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
27 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
28 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
29 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
30 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
31 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
32 * OTHER DEALINGS IN THE SOFTWARE.
33 *
34 * For more information, please refer to <http://unlicense.org/>
35 */
36
37 (function() {
38 'use strict';
39
40 var
41 /* jStorage version */
42 JSTORAGE_VERSION = '0.4.10',
43
44 /* detect a dollar object or create one if not found */
45 $ = window.jQuery || window.$ || (window.$ = {}),
46
47 /* check for a JSON handling support */
48 JSON = {
49 parse: window.JSON && (window.JSON.parse || window.JSON.decode) ||
50 String.prototype.evalJSON && function(str) {
51 return String(str).evalJSON();
52 } ||
53 $.parseJSON ||
54 $.evalJSON,
55 stringify: Object.toJSON ||
56 window.JSON && (window.JSON.stringify || window.JSON.encode) ||
57 $.toJSON
58 };
59
60 // Break if no JSON support was found
61 if (!('parse' in JSON) || !('stringify' in JSON)) {
62 throw new Error('No JSON support found, include //cdnjs.cloudflare.com/ajax/libs/json2/20110223/json2.js to page');
63 }
64
65 var
66 /* This is the object, that holds the cached values */
67 _storage = {
68 __jstorage_meta: {
69 CRC32: {}
70 }
71 },
72
73 /* Actual browser storage (localStorage or globalStorage['domain']) */
74 _storage_service = {
75 jStorage: '{}'
76 },
77
78 /* DOM element for older IE versions, holds userData behavior */
79 _storage_elm = null,
80
81 /* How much space does the storage take */
82 _storage_size = 0,
83
84 /* which backend is currently used */
85 _backend = false,
86
87 /* onchange observers */
88 _observers = {},
89
90 /* timeout to wait after onchange event */
91 _observer_timeout = false,
92
93 /* last update time */
94 _observer_update = 0,
95
96 /* pubsub observers */
97 _pubsub_observers = {},
98
99 /* skip published items older than current timestamp */
100 _pubsub_last = +new Date(),
101
102 /* Next check for TTL */
103 _ttl_timeout,
104
105 /**
106 * XML encoding and decoding as XML nodes can't be JSON'ized
107 * XML nodes are encoded and decoded if the node is the value to be saved
108 * but not if it's as a property of another object
109 * Eg. -
110 * $.jStorage.set('key', xmlNode); // IS OK
111 * $.jStorage.set('key', {xml: xmlNode}); // NOT OK
112 */
113 _XMLService = {
114
115 /**
116 * Validates a XML node to be XML
117 * based on jQuery.isXML function
118 */
119 isXML: function(elm) {
120 var documentElement = (elm ? elm.ownerDocument || elm : 0).documentElement;
121 return documentElement ? documentElement.nodeName !== 'HTML' : false;
122 },
123
124 /**
125 * Encodes a XML node to string
126 * based on http://www.mercurytide.co.uk/news/article/issues-when-working-ajax/
127 */
128 encode: function(xmlNode) {
129 if (!this.isXML(xmlNode)) {
130 return false;
131 }
132 try { // Mozilla, Webkit, Opera
133 return new XMLSerializer().serializeToString(xmlNode);
134 } catch (E1) {
135 try { // IE
136 return xmlNode.xml;
137 } catch (E2) {}
138 }
139 return false;
140 },
141
142 /**
143 * Decodes a XML node from string
144 * loosely based on http://outwestmedia.com/jquery-plugins/xmldom/
145 */
146 decode: function(xmlString) {
147 var dom_parser = ('DOMParser' in window && (new DOMParser()).parseFromString) ||
148 (window.ActiveXObject && function(_xmlString) {
149 var xml_doc = new ActiveXObject('Microsoft.XMLDOM');
150 xml_doc.async = 'false';
151 xml_doc.loadXML(_xmlString);
152 return xml_doc;
153 }),
154 resultXML;
155 if (!dom_parser) {
156 return false;
157 }
158 resultXML = dom_parser.call('DOMParser' in window && (new DOMParser()) || window, xmlString, 'text/xml');
159 return this.isXML(resultXML) ? resultXML : false;
160 }
161 };
162
163
164 ////////////////////////// PRIVATE METHODS ////////////////////////
165
166 /**
167 * Initialization function. Detects if the browser supports DOM Storage
168 * or userData behavior and behaves accordingly.
169 */
170 function _init() {
171 /* Check if browser supports localStorage */
172 var localStorageReallyWorks = false;
173 if ('localStorage' in window) {
174 try {
175 window.localStorage.setItem('_tmptest', 'tmpval');
176 localStorageReallyWorks = true;
177 window.localStorage.removeItem('_tmptest');
178 } catch (BogusQuotaExceededErrorOnIos5) {
179 // Thanks be to iOS5 Private Browsing mode which throws
180 // QUOTA_EXCEEDED_ERRROR DOM Exception 22.
181 }
182 }
183
184 if (localStorageReallyWorks) {
185 try {
186 if (window.localStorage) {
187 _storage_service = window.localStorage;
188 _backend = 'localStorage';
189 _observer_update = _storage_service.jStorage_update;
190 }
191 } catch (E3) { /* Firefox fails when touching localStorage and cookies are disabled */ }
192 }
193 /* Check if browser supports globalStorage */
194 else if ('globalStorage' in window) {
195 try {
196 if (window.globalStorage) {
197 if (window.location.hostname == 'localhost') {
198 _storage_service = window.globalStorage['localhost.localdomain'];
199 } else {
200 _storage_service = window.globalStorage[window.location.hostname];
201 }
202 _backend = 'globalStorage';
203 _observer_update = _storage_service.jStorage_update;
204 }
205 } catch (E4) { /* Firefox fails when touching localStorage and cookies are disabled */ }
206 }
207 /* Check if browser supports userData behavior */
208 else {
209 _storage_elm = document.createElement('link');
210 if (_storage_elm.addBehavior) {
211
212 /* Use a DOM element to act as userData storage */
213 _storage_elm.style.behavior = 'url(#default#userData)';
214
215 /* userData element needs to be inserted into the DOM! */
216 document.getElementsByTagName('head')[0].appendChild(_storage_elm);
217
218 try {
219 _storage_elm.load('jStorage');
220 } catch (E) {
221 // try to reset cache
222 _storage_elm.setAttribute('jStorage', '{}');
223 _storage_elm.save('jStorage');
224 _storage_elm.load('jStorage');
225 }
226
227 var data = '{}';
228 try {
229 data = _storage_elm.getAttribute('jStorage');
230 } catch (E5) {}
231
232 try {
233 _observer_update = _storage_elm.getAttribute('jStorage_update');
234 } catch (E6) {}
235
236 _storage_service.jStorage = data;
237 _backend = 'userDataBehavior';
238 } else {
239 _storage_elm = null;
240 return;
241 }
242 }
243
244 // Load data from storage
245 _load_storage();
246
247 // remove dead keys
248 _handleTTL();
249
250 // start listening for changes
251 _setupObserver();
252
253 // initialize publish-subscribe service
254 _handlePubSub();
255
256 // handle cached navigation
257 if ('addEventListener' in window) {
258 window.addEventListener('pageshow', function(event) {
259 if (event.persisted) {
260 _storageObserver();
261 }
262 }, false);
263 }
264 }
265
266 /**
267 * Reload data from storage when needed
268 */
269 function _reloadData() {
270 var data = '{}';
271
272 if (_backend == 'userDataBehavior') {
273 _storage_elm.load('jStorage');
274
275 try {
276 data = _storage_elm.getAttribute('jStorage');
277 } catch (E5) {}
278
279 try {
280 _observer_update = _storage_elm.getAttribute('jStorage_update');
281 } catch (E6) {}
282
283 _storage_service.jStorage = data;
284 }
285
286 _load_storage();
287
288 // remove dead keys
289 _handleTTL();
290
291 _handlePubSub();
292 }
293
294 /**
295 * Sets up a storage change observer
296 */
297 function _setupObserver() {
298 if (_backend == 'localStorage' || _backend == 'globalStorage') {
299 if ('addEventListener' in window) {
300 window.addEventListener('storage', _storageObserver, false);
301 } else {
302 document.attachEvent('onstorage', _storageObserver);
303 }
304 } else if (_backend == 'userDataBehavior') {
305 setInterval(_storageObserver, 1000);
306 }
307 }
308
309 /**
310 * Fired on any kind of data change, needs to check if anything has
311 * really been changed
312 */
313 function _storageObserver() {
314 var updateTime;
315 // cumulate change notifications with timeout
316 clearTimeout(_observer_timeout);
317 _observer_timeout = setTimeout(function() {
318
319 if (_backend == 'localStorage' || _backend == 'globalStorage') {
320 updateTime = _storage_service.jStorage_update;
321 } else if (_backend == 'userDataBehavior') {
322 _storage_elm.load('jStorage');
323 try {
324 updateTime = _storage_elm.getAttribute('jStorage_update');
325 } catch (E5) {}
326 }
327
328 if (updateTime && updateTime != _observer_update) {
329 _observer_update = updateTime;
330 _checkUpdatedKeys();
331 }
332
333 }, 25);
334 }
335
336 /**
337 * Reloads the data and checks if any keys are changed
338 */
339 function _checkUpdatedKeys() {
340 var oldCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32)),
341 newCrc32List;
342
343 _reloadData();
344 newCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32));
345
346 var key,
347 updated = [],
348 removed = [];
349
350 for (key in oldCrc32List) {
351 if (oldCrc32List.hasOwnProperty(key)) {
352 if (!newCrc32List[key]) {
353 removed.push(key);
354 continue;
355 }
356 if (oldCrc32List[key] != newCrc32List[key] && String(oldCrc32List[key]).substr(0, 2) == '2.') {
357 updated.push(key);
358 }
359 }
360 }
361
362 for (key in newCrc32List) {
363 if (newCrc32List.hasOwnProperty(key)) {
364 if (!oldCrc32List[key]) {
365 updated.push(key);
366 }
367 }
368 }
369
370 _fireObservers(updated, 'updated');
371 _fireObservers(removed, 'deleted');
372 }
373
374 /**
375 * Fires observers for updated keys
376 *
377 * @param {Array|String} keys Array of key names or a key
378 * @param {String} action What happened with the value (updated, deleted, flushed)
379 */
380 function _fireObservers(keys, action) {
381 keys = [].concat(keys || []);
382
383 var i, j, len, jlen;
384
385 if (action == 'flushed') {
386 keys = [];
387 for (var key in _observers) {
388 if (_observers.hasOwnProperty(key)) {
389 keys.push(key);
390 }
391 }
392 action = 'deleted';
393 }
394 for (i = 0, len = keys.length; i < len; i++) {
395 if (_observers[keys[i]]) {
396 for (j = 0, jlen = _observers[keys[i]].length; j < jlen; j++) {
397 _observers[keys[i]][j](keys[i], action);
398 }
399 }
400 if (_observers['*']) {
401 for (j = 0, jlen = _observers['*'].length; j < jlen; j++) {
402 _observers['*'][j](keys[i], action);
403 }
404 }
405 }
406 }
407
408 /**
409 * Publishes key change to listeners
410 */
411 function _publishChange() {
412 var updateTime = (+new Date()).toString();
413
414 if (_backend == 'localStorage' || _backend == 'globalStorage') {
415 try {
416 _storage_service.jStorage_update = updateTime;
417 } catch (E8) {
418 // safari private mode has been enabled after the jStorage initialization
419 _backend = false;
420 }
421 } else if (_backend == 'userDataBehavior') {
422 _storage_elm.setAttribute('jStorage_update', updateTime);
423 _storage_elm.save('jStorage');
424 }
425
426 _storageObserver();
427 }
428
429 /**
430 * Loads the data from the storage based on the supported mechanism
431 */
432 function _load_storage() {
433 /* if jStorage string is retrieved, then decode it */
434 if (_storage_service.jStorage) {
435 try {
436 _storage = JSON.parse(String(_storage_service.jStorage));
437 } catch (E6) {
438 _storage_service.jStorage = '{}';
439 }
440 } else {
441 _storage_service.jStorage = '{}';
442 }
443 _storage_size = _storage_service.jStorage ? String(_storage_service.jStorage).length : 0;
444
445 if (!_storage.__jstorage_meta) {
446 _storage.__jstorage_meta = {};
447 }
448 if (!_storage.__jstorage_meta.CRC32) {
449 _storage.__jstorage_meta.CRC32 = {};
450 }
451 }
452
453 /**
454 * This functions provides the 'save' mechanism to store the jStorage object
455 */
456 function _save() {
457 _dropOldEvents(); // remove expired events
458 try {
459 _storage_service.jStorage = JSON.stringify(_storage);
460 // If userData is used as the storage engine, additional
461 if (_storage_elm) {
462 _storage_elm.setAttribute('jStorage', _storage_service.jStorage);
463 _storage_elm.save('jStorage');
464 }
465 _storage_size = _storage_service.jStorage ? String(_storage_service.jStorage).length : 0;
466 } catch (E7) { /* probably cache is full, nothing is saved this way*/ }
467 }
468
469 /**
470 * Function checks if a key is set and is string or numberic
471 *
472 * @param {String} key Key name
473 */
474 function _checkKey(key) {
475 if (typeof key != 'string' && typeof key != 'number') {
476 throw new TypeError('Key name must be string or numeric');
477 }
478 if (key == '__jstorage_meta') {
479 throw new TypeError('Reserved key name');
480 }
481 return true;
482 }
483
484 /**
485 * Removes expired keys
486 */
487 function _handleTTL() {
488 var curtime, i, TTL, CRC32, nextExpire = Infinity,
489 changed = false,
490 deleted = [];
491
492 clearTimeout(_ttl_timeout);
493
494 if (!_storage.__jstorage_meta || typeof _storage.__jstorage_meta.TTL != 'object') {
495 // nothing to do here
496 return;
497 }
498
499 curtime = +new Date();
500 TTL = _storage.__jstorage_meta.TTL;
501
502 CRC32 = _storage.__jstorage_meta.CRC32;
503 for (i in TTL) {
504 if (TTL.hasOwnProperty(i)) {
505 if (TTL[i] <= curtime) {
506 delete TTL[i];
507 delete CRC32[i];
508 delete _storage[i];
509 changed = true;
510 deleted.push(i);
511 } else if (TTL[i] < nextExpire) {
512 nextExpire = TTL[i];
513 }
514 }
515 }
516
517 // set next check
518 if (nextExpire != Infinity) {
519 _ttl_timeout = setTimeout(_handleTTL, Math.min(nextExpire - curtime, 0x7FFFFFFF));
520 }
521
522 // save changes
523 if (changed) {
524 _save();
525 _publishChange();
526 _fireObservers(deleted, 'deleted');
527 }
528 }
529
530 /**
531 * Checks if there's any events on hold to be fired to listeners
532 */
533 function _handlePubSub() {
534 var i, len;
535 if (!_storage.__jstorage_meta.PubSub) {
536 return;
537 }
538 var pubelm,
539 _pubsubCurrent = _pubsub_last;
540
541 for (i = len = _storage.__jstorage_meta.PubSub.length - 1; i >= 0; i--) {
542 pubelm = _storage.__jstorage_meta.PubSub[i];
543 if (pubelm[0] > _pubsub_last) {
544 _pubsubCurrent = pubelm[0];
545 _fireSubscribers(pubelm[1], pubelm[2]);
546 }
547 }
548
549 _pubsub_last = _pubsubCurrent;
550 }
551
552 /**
553 * Fires all subscriber listeners for a pubsub channel
554 *
555 * @param {String} channel Channel name
556 * @param {Mixed} payload Payload data to deliver
557 */
558 function _fireSubscribers(channel, payload) {
559 if (_pubsub_observers[channel]) {
560 for (var i = 0, len = _pubsub_observers[channel].length; i < len; i++) {
561 // send immutable data that can't be modified by listeners
562 try {
563 _pubsub_observers[channel][i](channel, JSON.parse(JSON.stringify(payload)));
564 } catch (E) {}
565 }
566 }
567 }
568
569 /**
570 * Remove old events from the publish stream (at least 2sec old)
571 */
572 function _dropOldEvents() {
573 if (!_storage.__jstorage_meta.PubSub) {
574 return;
575 }
576
577 var retire = +new Date() - 2000;
578
579 for (var i = 0, len = _storage.__jstorage_meta.PubSub.length; i < len; i++) {
580 if (_storage.__jstorage_meta.PubSub[i][0] <= retire) {
581 // deleteCount is needed for IE6
582 _storage.__jstorage_meta.PubSub.splice(i, _storage.__jstorage_meta.PubSub.length - i);
583 break;
584 }
585 }
586
587 if (!_storage.__jstorage_meta.PubSub.length) {
588 delete _storage.__jstorage_meta.PubSub;
589 }
590
591 }
592
593 /**
594 * Publish payload to a channel
595 *
596 * @param {String} channel Channel name
597 * @param {Mixed} payload Payload to send to the subscribers
598 */
599 function _publish(channel, payload) {
600 if (!_storage.__jstorage_meta) {
601 _storage.__jstorage_meta = {};
602 }
603 if (!_storage.__jstorage_meta.PubSub) {
604 _storage.__jstorage_meta.PubSub = [];
605 }
606
607 _storage.__jstorage_meta.PubSub.unshift([+new Date(), channel, payload]);
608
609 _save();
610 _publishChange();
611 }
612
613
614 /**
615 * JS Implementation of MurmurHash2
616 *
617 * SOURCE: https://github.com/garycourt/murmurhash-js (MIT licensed)
618 *
619 * @author <a href='mailto:gary.court@gmail.com'>Gary Court</a>
620 * @see http://github.com/garycourt/murmurhash-js
621 * @author <a href='mailto:aappleby@gmail.com'>Austin Appleby</a>
622 * @see http://sites.google.com/site/murmurhash/
623 *
624 * @param {string} str ASCII only
625 * @param {number} seed Positive integer only
626 * @return {number} 32-bit positive integer hash
627 */
628
629 function murmurhash2_32_gc(str, seed) {
630 var
631 l = str.length,
632 h = seed ^ l,
633 i = 0,
634 k;
635
636 while (l >= 4) {
637 k =
638 ((str.charCodeAt(i) & 0xff)) |
639 ((str.charCodeAt(++i) & 0xff) << 8) |
640 ((str.charCodeAt(++i) & 0xff) << 16) |
641 ((str.charCodeAt(++i) & 0xff) << 24);
642
643 k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16));
644 k ^= k >>> 24;
645 k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16));
646
647 h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^ k;
648
649 l -= 4;
650 ++i;
651 }
652
653 switch (l) {
654 case 3:
655 h ^= (str.charCodeAt(i + 2) & 0xff) << 16;
656 case 2:
657 h ^= (str.charCodeAt(i + 1) & 0xff) << 8;
658 case 1:
659 h ^= (str.charCodeAt(i) & 0xff);
660 h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16));
661 }
662
663 h ^= h >>> 13;
664 h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16));
665 h ^= h >>> 15;
666
667 return h >>> 0;
668 }
669
670 ////////////////////////// PUBLIC INTERFACE /////////////////////////
671
672 $.jStorage = {
673 /* Version number */
674 version: JSTORAGE_VERSION,
675
676 /**
677 * Sets a key's value.
678 *
679 * @param {String} key Key to set. If this value is not set or not
680 * a string an exception is raised.
681 * @param {Mixed} value Value to set. This can be any value that is JSON
682 * compatible (Numbers, Strings, Objects etc.).
683 * @param {Object} [options] - possible options to use
684 * @param {Number} [options.TTL] - optional TTL value, in milliseconds
685 * @return {Mixed} the used value
686 */
687 set: function(key, value, options) {
688 _checkKey(key);
689
690 options = options || {};
691
692 // undefined values are deleted automatically
693 if (typeof value == 'undefined') {
694 this.deleteKey(key);
695 return value;
696 }
697
698 if (_XMLService.isXML(value)) {
699 value = {
700 _is_xml: true,
701 xml: _XMLService.encode(value)
702 };
703 } else if (typeof value == 'function') {
704 return undefined; // functions can't be saved!
705 } else if (value && typeof value == 'object') {
706 // clone the object before saving to _storage tree
707 value = JSON.parse(JSON.stringify(value));
708 }
709
710 _storage[key] = value;
711
712 _storage.__jstorage_meta.CRC32[key] = '2.' + murmurhash2_32_gc(JSON.stringify(value), 0x9747b28c);
713
714 this.setTTL(key, options.TTL || 0); // also handles saving and _publishChange
715
716 _fireObservers(key, 'updated');
717 return value;
718 },
719
720 /**
721 * Looks up a key in cache
722 *
723 * @param {String} key - Key to look up.
724 * @param {mixed} def - Default value to return, if key didn't exist.
725 * @return {Mixed} the key value, default value or null
726 */
727 get: function(key, def) {
728 _checkKey(key);
729 if (key in _storage) {
730 if (_storage[key] && typeof _storage[key] == 'object' && _storage[key]._is_xml) {
731 return _XMLService.decode(_storage[key].xml);
732 } else {
733 return _storage[key];
734 }
735 }
736 return typeof(def) == 'undefined' ? null : def;
737 },
738
739 /**
740 * Deletes a key from cache.
741 *
742 * @param {String} key - Key to delete.
743 * @return {Boolean} true if key existed or false if it didn't
744 */
745 deleteKey: function(key) {
746 _checkKey(key);
747 if (key in _storage) {
748 delete _storage[key];
749 // remove from TTL list
750 if (typeof _storage.__jstorage_meta.TTL == 'object' &&
751 key in _storage.__jstorage_meta.TTL) {
752 delete _storage.__jstorage_meta.TTL[key];
753 }
754
755 delete _storage.__jstorage_meta.CRC32[key];
756
757 _save();
758 _publishChange();
759 _fireObservers(key, 'deleted');
760 return true;
761 }
762 return false;
763 },
764
765 /**
766 * Sets a TTL for a key, or remove it if ttl value is 0 or below
767 *
768 * @param {String} key - key to set the TTL for
769 * @param {Number} ttl - TTL timeout in milliseconds
770 * @return {Boolean} true if key existed or false if it didn't
771 */
772 setTTL: function(key, ttl) {
773 var curtime = +new Date();
774 _checkKey(key);
775 ttl = Number(ttl) || 0;
776 if (key in _storage) {
777
778 if (!_storage.__jstorage_meta.TTL) {
779 _storage.__jstorage_meta.TTL = {};
780 }
781
782 // Set TTL value for the key
783 if (ttl > 0) {
784 _storage.__jstorage_meta.TTL[key] = curtime + ttl;
785 } else {
786 delete _storage.__jstorage_meta.TTL[key];
787 }
788
789 _save();
790
791 _handleTTL();
792
793 _publishChange();
794 return true;
795 }
796 return false;
797 },
798
799 /**
800 * Gets remaining TTL (in milliseconds) for a key or 0 when no TTL has been set
801 *
802 * @param {String} key Key to check
803 * @return {Number} Remaining TTL in milliseconds
804 */
805 getTTL: function(key) {
806 var curtime = +new Date(),
807 ttl;
808 _checkKey(key);
809 if (key in _storage && _storage.__jstorage_meta.TTL && _storage.__jstorage_meta.TTL[key]) {
810 ttl = _storage.__jstorage_meta.TTL[key] - curtime;
811 return ttl || 0;
812 }
813 return 0;
814 },
815
816 /**
817 * Deletes everything in cache.
818 *
819 * @return {Boolean} Always true
820 */
821 flush: function() {
822 _storage = {
823 __jstorage_meta: {
824 CRC32: {}
825 }
826 };
827 _save();
828 _publishChange();
829 _fireObservers(null, 'flushed');
830 return true;
831 },
832
833 /**
834 * Returns a read-only copy of _storage
835 *
836 * @return {Object} Read-only copy of _storage
837 */
838 storageObj: function() {
839 function F() {}
840 F.prototype = _storage;
841 return new F();
842 },
843
844 /**
845 * Returns an index of all used keys as an array
846 * ['key1', 'key2',..'keyN']
847 *
848 * @return {Array} Used keys
849 */
850 index: function() {
851 var index = [],
852 i;
853 for (i in _storage) {
854 if (_storage.hasOwnProperty(i) && i != '__jstorage_meta') {
855 index.push(i);
856 }
857 }
858 return index;
859 },
860
861 /**
862 * How much space in bytes does the storage take?
863 *
864 * @return {Number} Storage size in chars (not the same as in bytes,
865 * since some chars may take several bytes)
866 */
867 storageSize: function() {
868 return _storage_size;
869 },
870
871 /**
872 * Which backend is currently in use?
873 *
874 * @return {String} Backend name
875 */
876 currentBackend: function() {
877 return _backend;
878 },
879
880 /**
881 * Test if storage is available
882 *
883 * @return {Boolean} True if storage can be used
884 */
885 storageAvailable: function() {
886 return !!_backend;
887 },
888
889 /**
890 * Register change listeners
891 *
892 * @param {String} key Key name
893 * @param {Function} callback Function to run when the key changes
894 */
895 listenKeyChange: function(key, callback) {
896 _checkKey(key);
897 if (!_observers[key]) {
898 _observers[key] = [];
899 }
900 _observers[key].push(callback);
901 },
902
903 /**
904 * Remove change listeners
905 *
906 * @param {String} key Key name to unregister listeners against
907 * @param {Function} [callback] If set, unregister the callback, if not - unregister all
908 */
909 stopListening: function(key, callback) {
910 _checkKey(key);
911
912 if (!_observers[key]) {
913 return;
914 }
915
916 if (!callback) {
917 delete _observers[key];
918 return;
919 }
920
921 for (var i = _observers[key].length - 1; i >= 0; i--) {
922 if (_observers[key][i] == callback) {
923 _observers[key].splice(i, 1);
924 }
925 }
926 },
927
928 /**
929 * Subscribe to a Publish/Subscribe event stream
930 *
931 * @param {String} channel Channel name
932 * @param {Function} callback Function to run when the something is published to the channel
933 */
934 subscribe: function(channel, callback) {
935 channel = (channel || '').toString();
936 if (!channel) {
937 throw new TypeError('Channel not defined');
938 }
939 if (!_pubsub_observers[channel]) {
940 _pubsub_observers[channel] = [];
941 }
942 _pubsub_observers[channel].push(callback);
943 },
944
945 /**
946 * Publish data to an event stream
947 *
948 * @param {String} channel Channel name
949 * @param {Mixed} payload Payload to deliver
950 */
951 publish: function(channel, payload) {
952 channel = (channel || '').toString();
953 if (!channel) {
954 throw new TypeError('Channel not defined');
955 }
956
957 _publish(channel, payload);
958 },
959
960 /**
961 * Reloads the data from browser storage
962 */
963 reInit: function() {
964 _reloadData();
965 },
966
967 /**
968 * Removes reference from global objects and saves it as jStorage
969 *
970 * @param {Boolean} option if needed to save object as simple 'jStorage' in windows context
971 */
972 noConflict: function(saveInGlobal) {
973 delete window.$.jStorage;
974
975 if (saveInGlobal) {
976 window.jStorage = this;
977 }
978
979 return this;
980 }
981 };
982
983 // Initialize jStorage
984 _init();
985
986 })();