1 /* See license.txt for terms of usage */
  2 
  3 // ********************************************************************************************* //
  4 // Constants
  5 
  6 const Cc = Components.classes;
  7 const Ci = Components.interfaces;
  8 const Cr = Components.results;
  9 const Cu = Components.utils;
 10 
 11 const dirService = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
 12 
 13 // https://developer.mozilla.org/en/Using_JavaScript_code_modules
 14 var EXPORTED_SYMBOLS = ["Storage", "StorageService", "TextService"];
 15 
 16 Cu.import("resource://firebug/fbtrace.js");
 17 Cu.import("resource://gre/modules/FileUtils.jsm");
 18 
 19 // ********************************************************************************************* //
 20 // Implementation
 21 
 22 /**
 23  * http://dev.w3.org/html5/webstorage/#storage-0
 24  * interface Storage {
 25  *     readonly attribute unsigned long length;
 26  *     getter DOMString key(in unsigned long index);
 27  *     getter any getItem(in DOMString key);
 28  *     setter creator void setItem(in DOMString key, in any data);
 29  *     deleter void removeItem(in DOMString key);
 30  *     void clear();
 31  * };
 32  */
 33 function Storage(leafName)
 34 {
 35     this.leafName = leafName;
 36     this.objectTable = {};
 37 }
 38 
 39 Storage.prototype =
 40 {
 41     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
 42     // Read
 43 
 44     get length()
 45     {
 46         return this.key(-1);
 47     },
 48 
 49     key: function(index)
 50     {
 51         var i = 0;
 52         for (var p in this.objectTable)
 53         {
 54             if (i === index)
 55                 return p;
 56             i++;
 57         }
 58         return (index < 0 ? i : null);
 59     },
 60 
 61     getItem: function(key)
 62     {
 63         return this.objectTable[key];
 64     },
 65 
 66     getKeys: function()
 67     {
 68         var keys = [];
 69 
 70         for (var p in this.objectTable)
 71             if (this.objectTable.hasOwnProperty(p))
 72                 keys.push(p);
 73 
 74         return keys;
 75     },
 76 
 77     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
 78     // Write
 79 
 80     setItem: function(key, data)
 81     {
 82         this.objectTable[key] = data;
 83         StorageService.setStorage(this);
 84     },
 85 
 86     removeItem: function(key)
 87     {
 88         delete this.objectTable[key];
 89         StorageService.setStorage(this);
 90     },
 91 
 92     clear: function(now)
 93     {
 94         this.objectTable = {};
 95         StorageService.setStorage(this, now);
 96     }
 97 };
 98 
 99 // ********************************************************************************************* //
100 
101 /**
102  * var store = StorageService.getStorage(leafName);
103  * store.setItem("foo", bar);  // writes to disk
104  */
105 var StorageService =
106 {
107     getStorage: function(leafName)
108     {
109         var store = new Storage(leafName);
110 
111         try
112         {
113             var obj = ObjectPersister.readObject(leafName);
114             if (obj)
115                 store.objectTable = obj;
116         }
117         catch (err)
118         {
119             if (FBTrace.DBG_ERRORS || FBTrace.DBG_STORAGE)
120             {
121                 FBTrace.sysout("StorageService.getStorage; EXCEPTION for " + leafName +
122                     ": " + err, err);
123             }
124         }
125 
126         return store;
127     },
128 
129     setStorage: function(store, now)
130     {
131         if (!store || !store.leafName || !store.objectTable)
132             throw new Error("StorageService.setStorage requires Storage Object argument");
133 
134         if (now)
135             ObjectPersister.writeNow(store.leafName,  store.objectTable);
136         else
137             ObjectPersister.writeObject(store.leafName,  store.objectTable);
138     },
139 
140     removeStorage: function(leafName)
141     {
142         ObjectPersister.deleteObject(leafname);
143     }
144 };
145 
146 // ********************************************************************************************* //
147 // Implementation
148 
149 /**
150  * @class Represents an internal Firebug persistence service.
151  */
152 var ObjectPersister =
153 {
154     readObject: function(leafName)
155     {
156         if (FBTrace.DBG_STORAGE)
157             FBTrace.sysout("ObjectPersister read from leafName "+leafName);
158 
159         var file = FileUtils.getFile("ProfD", ["firebug", leafName]);
160 
161         if (!file.exists())
162         {
163             file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0666);
164             if (FBTrace.DBG_STORAGE)
165                 FBTrace.sysout("ObjectPersister.readTextFromFile file created " + file.path);
166             return;
167         }
168 
169         var obj = this.readObjectFromFile(file);
170         return obj;
171     },
172 
173     readObjectFromFile: function(file)
174     {
175         var text = ObjectPersister.readTextFromFile(file);
176         if (!text)
177             return null;
178 
179         var obj = JSON.parse(text);
180         if (!obj)
181             return;
182 
183         if (FBTrace.DBG_STORAGE)
184             FBTrace.sysout("PersistedObject loaded from " + file.path+" got text "+text, obj);
185 
186         return obj;
187     },
188 
189     readTextFromFile: function(file)
190     {
191         try
192         {
193             var inputStream = Cc["@mozilla.org/network/file-input-stream;1"]
194                 .createInstance(Ci.nsIFileInputStream);
195             var cstream = Cc["@mozilla.org/intl/converter-input-stream;1"]
196                 .createInstance(Ci.nsIConverterInputStream);
197 
198             // Initialize input stream.
199             inputStream.init(file, 0x01 | 0x08, 0666, 0); // read, create
200             cstream.init(inputStream, "UTF-8", 0, 0);
201 
202             // Load  json.
203             var json = "";
204             var data = {};
205             while (cstream.readString(-1, data) != 0)
206                 json += data.value;
207 
208             inputStream.close();
209 
210             return json;
211         }
212         catch (err)
213         {
214             if (FBTrace.DBG_ERRORS || FBTrace.DBG_STORAGE)
215                 FBTrace.sysout("ObjectPersister.initialize; EXCEPTION", err);
216         }
217     },
218 
219     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
220 
221     // Batch the writes for each event loop
222     writeDelay: 250,
223 
224     writeObject: function(leafName, obj)
225     {
226         if (this.isPrivateBrowsing())
227             throw new Error("No storage is written while in private browsing mode");
228 
229         if (ObjectPersister.flushTimeout)
230             return;
231 
232         ObjectPersister.flushTimeout = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
233 
234         var writeOnTimeout = ObjectPersister.getWriteOnTimeout(leafName, obj);
235         ObjectPersister.flushTimeout.init(writeOnTimeout, ObjectPersister.writeDelay,
236             Ci.nsITimer.TYPE_ONE_SHOT);
237     },
238 
239     getWriteOnTimeout: function(leafName, obj)
240     {
241         var writerClosure =
242         {
243             leafName: leafName,
244             obj: obj,
245             observe: function(timer, topic, data)
246             {
247                 ObjectPersister.writeNow(writerClosure.leafName, writerClosure.obj);
248                 delete ObjectPersister.flushTimeout;
249             }
250         };
251         return writerClosure;
252     },
253 
254     writeNow: function(leafName, obj)
255     {
256         try
257         {
258             // Convert data to JSON.
259             var jsonString = JSON.stringify(obj);
260             var file = FileUtils.getFile("ProfD", ["firebug", leafName]);
261             ObjectPersister.writeTextToFile(file, jsonString);
262         }
263         catch(exc)
264         {
265             if (FBTrace.DBG_ERRORS || FBTrace.DBG_STORAGE)
266                 FBTrace.sysout("ObjectPersister.writeNow; EXCEPTION for " + leafName + ": " +
267                     exc, {exception: exc, obj: obj});
268         }
269     },
270 
271     writeTextToFile: function(file, string)
272     {
273         try
274         {
275             // Initialize output stream.
276             var outputStream = Cc["@mozilla.org/network/file-output-stream;1"]
277                 .createInstance(Ci.nsIFileOutputStream);
278             outputStream.init(file, 0x02 | 0x08 | 0x20, 0666, 0); // write, create, truncate
279 
280             // Store JSON
281             outputStream.write(string, string.length);
282             outputStream.close();
283 
284             if (FBTrace.DBG_STORAGE)
285                 FBTrace.sysout("ObjectPersister.writeNow to " + file.path, string);
286 
287             return file.path;
288         }
289         catch (err)
290         {
291             if (FBTrace.DBG_ERRORS || FBTrace.DBG_STORAGE)
292                 FBTrace.sysout("ObjectPersister.writeTextToFile; EXCEPTION for " + file.path +
293                     ": "+err, {exception: err, string: string});
294         }
295     },
296 
297     isPrivateBrowsing: function()
298     {
299         try
300         {
301             // Unfortunatelly the "firebug/chrome/privacy" module can't be used
302             // since this scope is JavaScript code module.
303             // xxxHonza: storageService should be converted into AMD (but it's used
304             // in firebug-service, which is also JS code module).
305             var pbs = Components.classes["@mozilla.org/privatebrowsing;1"]
306                 .getService(Components.interfaces.nsIPrivateBrowsingService);
307             return pbs.privateBrowsingEnabled;
308         }
309         catch (e)
310         {
311         }
312 
313         return false;
314     }
315 };
316 
317 // ********************************************************************************************* //
318 
319 var TextService =
320 {
321     readText: ObjectPersister.readTextFromFile,
322     writeText: ObjectPersister.writeTextToFile
323 };
324 
325 // ********************************************************************************************* //
326