Storage changes:
[taboo.git] / components / oyTaboo.js
blobe418c756c3bd35173f37e3e8e3202a219dc30536
1 /*
2  * Copyright 2007 Jesse Andrews and Manish Singh
3  *  
4  * This file may be used under the terms of of the
5  * GNU General Public License Version 2 or later (the "GPL"),
6  * http://www.gnu.org/licenses/gpl.html
7  *  
8  * Software distributed under the License is distributed on an "AS IS" basis,
9  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
10  * for the specific language governing rights and limitations under the
11  * License.
12  * 
13  * Portions are derived from the Mozilla nsSessionStore component:
14  *
15  * Copyright (C) 2006 Simon Bünzli <zeniko@gmail.com>
16  *
17  * Contributor(s):
18  * Dietrich Ayala <autonome@gmail.com>
19  */
21 const TB_CONTRACTID = '@oy/taboo;1';
22 const TB_CLASSID    = Components.ID('{962a9516-b177-4083-bbe8-e10f47cf8570}');
23 const TB_CLASSNAME  = 'Taboo Service';
26 const Cc = Components.classes;
27 const Ci = Components.interfaces; 
28 const Cr = Components.results;
29 const Cu = Components.utils;
31 /* from nspr's prio.h */
32 const PR_RDONLY      = 0x01;
33 const PR_WRONLY      = 0x02;
34 const PR_RDWR        = 0x04;
35 const PR_CREATE_FILE = 0x08;
36 const PR_APPEND      = 0x10;
37 const PR_TRUNCATE    = 0x20;
38 const PR_SYNC        = 0x40;
39 const PR_EXCL        = 0x80;
41 const CAPABILITIES = [
42   "Subframes", "Plugins", "Javascript", "MetaRedirects", "Images"
46 function getObserverService() {
47   return Cc['@mozilla.org/observer-service;1']
48     .getService(Ci.nsIObserverService);
51 function getBoolPref(prefName, defaultValue) {
52   try {
53     var prefs = Cc['@mozilla.org/preferences-service;1']
54       .getService(Ci.nsIPrefBranch);
55     return prefs.getBoolPref(prefName);
56   }
57   catch (e) {
58     return defaultValue;
59   }
63 /* MD5 wrapper */
64 function hex_md5_stream(stream) {   
65   var hasher = Components.classes["@mozilla.org/security/hash;1"]
66     .createInstance(Components.interfaces.nsICryptoHash);
67   hasher.init(hasher.MD5);
69   hasher.updateFromStream(stream, stream.available());
70   var hash = hasher.finish(false);
72   var ret = '';
73   for (var i = 0; i < hash.length; ++i) {
74     var hexChar = hash.charCodeAt(i).toString(16);
75     if (hexChar.length == 1)
76       ret += '0';
77     ret += hexChar;
78   }
80   return ret;
83 function hex_md5(s) {
84   var stream = Components.classes["@mozilla.org/io/string-input-stream;1"]
85     .createInstance(Components.interfaces.nsIStringInputStream);
86   stream.setData(s, s.length);
88   return hex_md5_stream(stream);
93  * Taboo Info Instance
94  */
96 function TabooInfo(url, title, description, imageURL, created, updated, data) {
97   this.url = url;
98   this.title = title;
99   this.description = description;
100   this.imageURL = imageURL;
101   this.created = new Date(created);
102   this.updated = new Date(updated);
103   this.data = data;
106 TabooInfo.prototype = {
107   QueryInterface: function(iid) {
108     if (!iid.equals(Ci.nsISupports) &&
109         !iid.equals(Ci.oyITabooInfo)) {
110       throw Cr.NS_ERROR_NO_INTERFACE;
111     }
112     return this;
113   }
117  * Taboo Service Component
118  */
121 function snapshot() {
122   var wm = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
123   var win = wm.getMostRecentWindow('navigator:browser');
124   var content = win.content;
126   var canvas = win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
128   var realW = content.document.body.clientWidth;
129   var realH = content.innerHeight;
131   var pW = 500.0/realW;
132   var pH = 500.0/realH;
134   var p = pW;
136   if (pH < pW) {
137     p = pH;
138   }
140   var w = p * realW;
141   var h = p * realH;
143   canvas.setAttribute("width", Math.floor(w));
144   canvas.setAttribute("height", Math.floor(h));
145   
146   var ctx = canvas.getContext("2d");
147   ctx.scale(p, p);
148   ctx.drawWindow(content, content.scrollX, content.scrollY, realW, realH, "rgb(0,0,0)");
150   return [win, canvas];
154 function createStatement(dbconn, sql) {
155   var stmt = dbconn.createStatement(sql);
156   var wrapper = Cc["@mozilla.org/storage/statement-wrapper;1"]
157     .createInstance(Ci.mozIStorageStatementWrapper);
159   wrapper.initialize(stmt);
160   return wrapper;
163 function TabooStorageSQL() {
164   this._tabooDir = Cc['@mozilla.org/file/directory_service;1']
165     .getService(Ci.nsIProperties).get('ProfD', Ci.nsILocalFile);
166   this._tabooDir.append('taboo');
168   if (!this._tabooDir.exists())
169     this._tabooDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0700);
171   var dbfile = this._tabooDir.clone();
172   dbfile.append('taboo.sqlite');
174   var storageService = Cc['@mozilla.org/storage/service;1']
175     .getService(Ci.mozIStorageService);
176   this._DBConn = storageService.openDatabase(dbfile);
178   var schema = 'url TEXT PRIMARY KEY, title TEXT, description TEXT, ' +
179                'md5 TEXT, favicon TEXT, full TEXT, ' +
180                'created INTEGER, updated INTEGER, deleted INTEGER';
182   try {
183     this._DBConn.createTable('taboo_data', schema);
184   }
185   catch (e) { }
187   this._fetchData = createStatement(this._DBConn,
188     'SELECT * FROM taboo_data WHERE url = :url');
190   this._markDelete = createStatement(this._DBConn,
191     'UPDATE taboo_data SET deleted = :deleted WHERE url = :url');
192   this._removeURL = createStatement(this._DBConn,
193     'DELETE FROM taboo_data WHERE url = :url');
195   this._insertURL = createStatement(this._DBConn,
196     'INSERT INTO taboo_data ' +
197     '(url, title, description, md5, favicon, full, created, updated) ' +
198     'VALUES ' +
199     '(:url, :title, :description, :md5, :favicon, :full, :created, :updated)');
200   this._updateURL = createStatement(this._DBConn,
201     'UPDATE taboo_data SET title = :title, favicon = :favicon, ' +
202     'full = :full, updated = :updated WHERE url = :url');
203   this._updateDesc = createStatement(this._DBConn,
204     'UPDATE taboo_data SET description = :description WHERE url = :url');
207 TabooStorageSQL.prototype = {
208   save: function TSSQL_save(url, description, data, preview) {
209     var title = data.entries[data.index - 1].title;
211     if (!title) {
212       var ios = Cc['@mozilla.org/network/io-service;1']
213         .getService(Ci.nsIIOService);
214       var uri = ios.newURI(url, null, null);
216       if (uri.path.length > 1) {
217         var parts = uri.path.split('/');
218         title = parts.pop();
219       }
220         
221       if (!title)
222         title = uri.host;
223     }
225     var updated = Date.now();
227     var stmt = this._fetchData;
228     stmt.reset();
229     stmt.params.url = url;
231     var exists = stmt.step();
233     var md5;
234     if (exists)
235       md5 = stmt.row.md5;
236     else
237       md5 = hex_md5(url);
239     stmt.reset();
241     if (exists) {
242       var pp = this._updateURL.params;
243       pp.url = url;
244       pp.title = title;
245       pp.updated = updated;
246       pp.full = data.toSource();
248       this._updateURL.step();
249       this._updateURL.reset();
251       if (description) {
252         pp = this._updateDesc.params;
253         pp.url = url;
254         pp.description = description;
256         this._updateDesc.step();
257         this._updateDesc.reset();
258       } 
259     } else {
260       var pp = this._insertURL.params;
261       pp.url = url;
262       pp.title = title;
263       pp.description = description;
264       pp.md5 = hex_md5(url);
265       pp.full = data.toSource();
266       pp.created = updated;
267       pp.updated = updated;
269       this._insertURL.step();
270       this._insertURL.reset();
271     }
273     try {
274       var file = this._getPreviewFile(md5);
276       var ostream = Cc['@mozilla.org/network/file-output-stream;1']
277         .createInstance(Ci.nsIFileOutputStream);
278       ostream.init(file, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, 0600, 0);
280       ostream.write(preview, preview.length);
281       ostream.close();
282     }
283     catch (e) { } 
284   },
285   delete: function TSSQL_delete(url) {
286     this._markDelete.params.url = url;
287     this._markDelete.params.deleted = Date.now();
288     this._markDelete.step();
289     this._markDelete.reset();
290   },
291   reallyDelete: function TSSQL_reallyDelete(url) {
292     this._removeURL.params.url = url;
293     this._removeURL.step();
294     this._removeURL.reset();
295   },
296   retrieve: function TSSQL_retrieve(url) {
297     var stmt = this._fetchData;
298     stmt.reset();
299     stmt.params.url = url;
301     if (!stmt.step())
302       return null;
304     var ios = Cc['@mozilla.org/network/io-service;1']
305       .getService(Ci.nsIIOService);
306     var fileHandler = ios.getProtocolHandler('file')
307       .QueryInterface(Ci.nsIFileProtocolHandler);
308     var imageFile = this._getPreviewFile(stmt.row.md5);
309     var imageURL = fileHandler.getURLSpecFromFile(imageFile);
311     var data = stmt.row.full.replace(/\r\n?/g, '\n');
312     var sandbox = new Cu.Sandbox('about:blank');
313     var state = Cu.evalInSandbox(data, sandbox);
315     var ret = new TabooInfo(url, stmt.row.title, stmt.row.description,
316                             imageURL, stmt.row.created, stmt.row.updated,
317                             state);
318     stmt.reset();
320     return ret;
321   },
322   getURLs: function TSSQL_getURLs(filter) {
323     var sql = 'SELECT url FROM taboo_data';
324     if (filter) {
325       var where = ' WHERE url LIKE "%FILTER%" or title LIKE "%FILTER%" or ' +
326                   'description LIKE "%FILTER%"';
327       sql += where.replace(/FILTER/g, filter);
328     }
330     sql += " order by updated desc";
331     dump("SQL: " + sql + "\n");
333     var stmt = createStatement(this._DBConn, sql);
335     var urls = [];
336     while (stmt.step()) {
337       urls.push(stmt.row.url);
338       dump("URLY: " + stmt.row.url + "\n");
339     }
341     stmt.reset();
342     return urls;
343   },
344   _getPreviewFile: function TSSQL__getPreviewFile(id) {
345     var file = this._tabooDir.clone();
346     file.append(id + '.png');
347     return file;
348   }
352 function TabooService() {
353   var obs = getObserverService();
354   obs.addObserver(this, 'profile-after-change', false);
357 TabooService.prototype = {
358   _init: function TB__init() {
359     this._storage = new TabooStorageSQL();
360   },
361   observe: function TB_observe(subject, topic, state) {
362     var obs = getObserverService();
364     switch (topic) {
365       case 'profile-after-change':
366         obs.removeObserver(this, 'profile-after-change');
367         this._init();
368         break;
369     }
370   },
372   save: function TB_save(aDescription) {
373     var win, canvas;
374     [win, canvas] = snapshot();
376     var tabbrowser = win.getBrowser();
377     var selectedBrowser = tabbrowser.selectedBrowser;
379     var currentTab = -1;
380     var browsers = tabbrowser.browsers;
381     for (var i = 0; i < browsers.length; i++) {
382       if (browsers[i] == selectedBrowser)
383         currentTab = i;
384     }
386     if (currentTab == -1)
387       return false;
389     var ss = Cc['@mozilla.org/browser/sessionstore;1']
390       .getService(Ci.nsISessionStore);
391     var winJSON = "(" + ss.getWindowState(win) + ")";
393     var sandbox = new Cu.Sandbox('about:blank');
394     var winState = Cu.evalInSandbox(winJSON, sandbox);
396     var state = winState.windows[0].tabs[currentTab];
398     var previewData = canvas.toDataURL();
399     var preview = win.atob(previewData.substr('data:image/png;base64,'.length));
401     var url = selectedBrowser.currentURI.spec;
403     this._storage.save(url, aDescription, state, preview);
405     return true;
406   },
407   delete: function TB_delete(aURL) {
408     this._storage.delete(aURL);
409   },
410   get: function TB_get(filter) {
411     var urls = this._storage.getURLs(filter);
413     var enumerator = {
414       _urls: urls,
415       _storage: this._storage,
416       getNext: function() {
417         var url = this._urls.shift();
418         return this._storage.retrieve(url);
419       },
420       hasMoreElements: function() {
421         return this._urls.length > 0;
422       }
423     }
425     return enumerator;
426   },
428   /* Because sessionstore doesn't let us restore a single tab, we cut'n'paste
429    * a bunch of code here
430    */
431   open: function TB_open(aURL, aWhere) {
432     var info = this._storage.retrieve(aURL);
433     var tabData = info.data;
435     // helper hash for ensuring unique frame IDs
436     var idMap = { used: {} };
438     var wm = Cc['@mozilla.org/appshell/window-mediator;1']
439       .getService(Ci.nsIWindowMediator);
440     var win = wm.getMostRecentWindow('navigator:browser');
442     var loadInBackground = getBoolPref("browser.tabs.loadBookmarksInBackground", false);
444     var tabbrowser = win.getBrowser();
446     var tab;
447     switch (aWhere) {
448       case 'current':
449         tab = tabbrowser.mCurrentTab;
450         break;
451       case 'tabshifted':
452         loadInBackground = !loadInBackground;
453         // fall through
454       case 'tab':
455         tab = tabbrowser.loadOneTab('about:blank', null, null, null,
456                                     loadInBackground, false);
457         break;
458       default:
459         return;
460     }
462     var _this = this;
464     var browser = win.getBrowser().getBrowserForTab(tab);
465     var history = browser.webNavigation.sessionHistory;
467     if (history.count > 0) {
468       history.PurgeHistory(history.count);
469     }
470     history.QueryInterface(Ci.nsISHistoryInternal);
472     browser.markupDocumentViewer.textZoom = parseFloat(tabData.zoom || 1);
474     for (var i = 0; i < tabData.entries.length; i++) {
475       history.addEntry(this._deserializeHistoryEntry(tabData.entries[i], idMap), true);
476     }
478     // make sure to reset the capabilities and attributes, in case this tab gets reused
479     var disallow = (tabData.disallow)?tabData.disallow.split(","):[];
480     CAPABILITIES.forEach(function(aCapability) {
481       browser.docShell["allow" + aCapability] = disallow.indexOf(aCapability) == -1;
482     });
483     Array.filter(tab.attributes, function(aAttr) {
484       return (_this.xulAttributes.indexOf(aAttr.name) > -1);
485     }).forEach(tab.removeAttribute, tab);
486     if (tabData.xultab) {
487       tabData.xultab.split(" ").forEach(function(aAttr) {
488         if (/^([^\s=]+)=(.*)/.test(aAttr)) {
489           tab.setAttribute(RegExp.$1, decodeURI(RegExp.$2));
490         }
491       });
492     }
494     // notify the tabbrowser that the tab chrome has been restored
495     var event = win.document.createEvent("Events");
496     event.initEvent("SSTabRestoring", true, false);
497     tab.dispatchEvent(event);
499     var activeIndex = (tabData.index || tabData.entries.length) - 1;
500     try {
501       browser.webNavigation.gotoIndex(activeIndex);
502     }
503     catch (ex) { } // ignore an invalid tabData.index
505     // restore those aspects of the currently active documents
506     // which are not preserved in the plain history entries
507     // (mainly scroll state and text data)
508     browser.__SS_restore_data = tabData.entries[activeIndex] || {};
509     browser.__SS_restore_text = tabData.text || "";
510     browser.__SS_restore_tab = tab;
511     browser.__SS_restore = this.restoreDocument_proxy;
512     browser.addEventListener("load", browser.__SS_restore, true);
513   },
514   _deserializeHistoryEntry: function TB__deserializeHistoryEntry(aEntry, aIdMap) {
515     var shEntry = Cc["@mozilla.org/browser/session-history-entry;1"].
516                   createInstance(Ci.nsISHEntry);
517     
518     var ioService = Cc["@mozilla.org/network/io-service;1"].
519                     getService(Ci.nsIIOService);
520     shEntry.setURI(ioService.newURI(aEntry.url, null, null));
521     shEntry.setTitle(aEntry.title || aEntry.url);
522     shEntry.setIsSubFrame(aEntry.subframe || false);
523     shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory;
524     
525     if (aEntry.cacheKey) {
526       var cacheKey = Cc["@mozilla.org/supports-PRUint32;1"].
527                      createInstance(Ci.nsISupportsPRUint32);
528       cacheKey.data = aEntry.cacheKey;
529       shEntry.cacheKey = cacheKey;
530     }
531     if (aEntry.ID) {
532       // get a new unique ID for this frame (since the one from the last
533       // start might already be in use)
534       var id = aIdMap[aEntry.ID] || 0;
535       if (!id) {
536         for (id = Date.now(); aIdMap.used[id]; id++);
537         aIdMap[aEntry.ID] = id;
538         aIdMap.used[id] = true;
539       }
540       shEntry.ID = id;
541     }
542     
543     var scrollPos = (aEntry.scroll || "0,0").split(",");
544     scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0];
545     shEntry.setScrollPosition(scrollPos[0], scrollPos[1]);
546     
547     if (aEntry.postdata) {
548       var stream = Cc["@mozilla.org/io/string-input-stream;1"].
549                    createInstance(Ci.nsIStringInputStream);
550       stream.setData(aEntry.postdata, -1);
551       shEntry.postData = stream;
552     }
553     
554     if (aEntry.children && shEntry instanceof Ci.nsISHContainer) {
555       for (var i = 0; i < aEntry.children.length; i++) {
556         shEntry.AddChild(this._deserializeHistoryEntry(aEntry.children[i], aIdMap), i);
557       }
558     }
559     
560     return shEntry;
561   },
562   restoreDocument_proxy: function TB_restoreDocument_proxy(aEvent) {
563     // wait for the top frame to be loaded completely
564     if (!aEvent || !aEvent.originalTarget || !aEvent.originalTarget.defaultView || aEvent.originalTarget.defaultView != aEvent.originalTarget.defaultView.top) {
565       return;
566     }
567     
568     var textArray = this.__SS_restore_text ? this.__SS_restore_text.split(" ") : [];
569     function restoreTextData(aContent, aPrefix) {
570       textArray.forEach(function(aEntry) {
571         if (/^((?:\d+\|)*)(#?)([^\s=]+)=(.*)$/.test(aEntry) && (!RegExp.$1 || RegExp.$1 == aPrefix)) {
572           var document = aContent.document;
573           var node = RegExp.$2 ? document.getElementById(RegExp.$3) : document.getElementsByName(RegExp.$3)[0] || null;
574           if (node && "value" in node) {
575             node.value = decodeURI(RegExp.$4);
576             
577             var event = document.createEvent("UIEvents");
578             event.initUIEvent("input", true, true, aContent, 0);
579             node.dispatchEvent(event);
580           }
581         }
582       });
583     }
584     
585     function restoreTextDataAndScrolling(aContent, aData, aPrefix) {
586       restoreTextData(aContent, aPrefix);
587       if (aData.innerHTML) {
588         aContent.setTimeout(function(aHTML) { if (this.document.designMode == "on") { this.document.body.innerHTML = aHTML; } }, 0, aData.innerHTML);
589       }
590       if (aData.scroll && /(\d+),(\d+)/.test(aData.scroll)) {
591         aContent.scrollTo(RegExp.$1, RegExp.$2);
592       }
593       for (var i = 0; i < aContent.frames.length; i++) {
594         if (aData.children && aData.children[i]) {
595           restoreTextDataAndScrolling(aContent.frames[i], aData.children[i], i + "|" + aPrefix);
596         }
597       }
598     }
599     
600     var content = XPCNativeWrapper(aEvent.originalTarget).defaultView;
601     if (this.currentURI.spec == "about:config") {
602       // unwrap the document for about:config because otherwise the properties
603       // of the XBL bindings - as the textbox - aren't accessible (see bug 350718)
604       content = content.wrappedJSObject;
605     }
606     restoreTextDataAndScrolling(content, this.__SS_restore_data, "");
607     
608     // notify the tabbrowser that this document has been completely restored
609     var event = this.ownerDocument.createEvent("Events");
610     event.initEvent("SSTabRestored", true, false);
611     this.__SS_restore_tab.dispatchEvent(event);
612     
613     this.removeEventListener("load", this.__SS_restore, true);
614     delete this.__SS_restore_data;
615     delete this.__SS_restore_text;
616     delete this.__SS_restore_tab;
617   },
618   xulAttributes: [],
620   getInterfaces: function TB_getInterfaces(countRef) {
621     var interfaces = [Ci.oyITaboo, Ci.nsIObserver, Ci.nsISupports];
622     countRef.value = interfaces.length;
623     return interfaces;
624   },
625   getHelperForLanguage: function TB_getHelperForLanguage(language) {
626     return null;
627   },
628   contractID: TB_CONTRACTID,
629   classDescription: TB_CLASSNAME,
630   classID: TB_CLASSID,
631   implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,
632   flags: Ci.nsIClassInfo.SINGLETON,
634   QueryInterface: function TB_QueryInterface(iid) {
635     if (iid.equals(Ci.oyITaboo) ||
636         iid.equals(Ci.nsIObserver) ||
637         iid.equals(Ci.nsISupports))
638       return this;
639     throw Cr.NS_ERROR_NO_INTERFACE;
640   }
643 function GenericComponentFactory(ctor) {
644   this._ctor = ctor;
647 GenericComponentFactory.prototype = {
649   _ctor: null,
651   // nsIFactory
652   createInstance: function(outer, iid) {
653     if (outer != null)
654       throw Cr.NS_ERROR_NO_AGGREGATION;
655     return (new this._ctor()).QueryInterface(iid);
656   },
658   // nsISupports
659   QueryInterface: function(iid) {
660     if (iid.equals(Ci.nsIFactory) ||
661         iid.equals(Ci.nsISupports))
662       return this;
663     throw Cr.NS_ERROR_NO_INTERFACE;
664   },
667 var Module = {
668   QueryInterface: function(iid) {
669     if (iid.equals(Ci.nsIModule) ||
670         iid.equals(Ci.nsISupports))
671       return this;
673     throw Cr.NS_ERROR_NO_INTERFACE;
674   },
676   getClassObject: function(cm, cid, iid) {
677     if (!iid.equals(Ci.nsIFactory))
678       throw Cr.NS_ERROR_NOT_IMPLEMENTED;
680     if (cid.equals(TB_CLASSID))
681       return new GenericComponentFactory(TabooService)
683     throw Cr.NS_ERROR_NO_INTERFACE;
684   },
686   registerSelf: function(cm, file, location, type) {
687     var cr = cm.QueryInterface(Ci.nsIComponentRegistrar);
688     cr.registerFactoryLocation(TB_CLASSID, TB_CLASSNAME, TB_CONTRACTID,
689                                file, location, type);
691     var catman = Cc['@mozilla.org/categorymanager;1']
692       .getService(Ci.nsICategoryManager);
693     catman.addCategoryEntry('app-startup', TB_CLASSNAME,
694                             'service,' + TB_CONTRACTID,
695                             true, true);
696   },
698   unregisterSelf: function(cm, location, type) {
699     var cr = cm.QueryInterface(Ci.nsIComponentRegistrar);
700     cr.unregisterFactoryLocation(TB_CLASSID, location);
701   },
703   canUnload: function(cm) {
704     return true;
705   },
708 function NSGetModule(compMgr, fileSpec)
710   return Module;