2 * Copyright 2007 Jesse Andrews and Manish Singh
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
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
13 * Portions are derived from the Mozilla nsSessionStore component:
15 * Copyright (C) 2006 Simon Bünzli <zeniko@gmail.com>
18 * Dietrich Ayala <autonome@gmail.com>
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;
35 const PR_CREATE_FILE = 0x08;
36 const PR_APPEND = 0x10;
37 const PR_TRUNCATE = 0x20;
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) {
53 var prefs = Cc['@mozilla.org/preferences-service;1']
54 .getService(Ci.nsIPrefBranch);
55 return prefs.getBoolPref(prefName);
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);
73 for (var i = 0; i < hash.length; ++i) {
74 var hexChar = hash.charCodeAt(i).toString(16);
75 if (hexChar.length == 1)
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);
96 function TabooInfo(url, title, description, imageURL, created, updated, data) {
99 this.description = description;
100 this.imageURL = imageURL;
101 this.created = new Date(created);
102 this.updated = new Date(updated);
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;
117 * Taboo Service Component
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;
143 canvas.setAttribute("width", Math.floor(w));
144 canvas.setAttribute("height", Math.floor(h));
146 var ctx = canvas.getContext("2d");
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);
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';
183 this._DBConn.createTable('taboo_data', schema);
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) ' +
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;
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('/');
225 var updated = Date.now();
227 var stmt = this._fetchData;
229 stmt.params.url = url;
231 var exists = stmt.step();
242 var pp = this._updateURL.params;
245 pp.updated = updated;
246 pp.full = data.toSource();
248 this._updateURL.step();
249 this._updateURL.reset();
252 pp = this._updateDesc.params;
254 pp.description = description;
256 this._updateDesc.step();
257 this._updateDesc.reset();
260 var pp = this._insertURL.params;
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();
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);
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();
291 reallyDelete: function TSSQL_reallyDelete(url) {
292 this._removeURL.params.url = url;
293 this._removeURL.step();
294 this._removeURL.reset();
296 retrieve: function TSSQL_retrieve(url) {
297 var stmt = this._fetchData;
299 stmt.params.url = url;
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,
322 getURLs: function TSSQL_getURLs(filter) {
323 var sql = 'SELECT url FROM taboo_data';
325 var where = ' WHERE url LIKE "%FILTER%" or title LIKE "%FILTER%" or ' +
326 'description LIKE "%FILTER%"';
327 sql += where.replace(/FILTER/g, filter);
330 sql += " order by updated desc";
331 dump("SQL: " + sql + "\n");
333 var stmt = createStatement(this._DBConn, sql);
336 while (stmt.step()) {
337 urls.push(stmt.row.url);
338 dump("URLY: " + stmt.row.url + "\n");
344 _getPreviewFile: function TSSQL__getPreviewFile(id) {
345 var file = this._tabooDir.clone();
346 file.append(id + '.png');
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();
361 observe: function TB_observe(subject, topic, state) {
362 var obs = getObserverService();
365 case 'profile-after-change':
366 obs.removeObserver(this, 'profile-after-change');
372 save: function TB_save(aDescription) {
374 [win, canvas] = snapshot();
376 var tabbrowser = win.getBrowser();
377 var selectedBrowser = tabbrowser.selectedBrowser;
380 var browsers = tabbrowser.browsers;
381 for (var i = 0; i < browsers.length; i++) {
382 if (browsers[i] == selectedBrowser)
386 if (currentTab == -1)
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);
407 delete: function TB_delete(aURL) {
408 this._storage.delete(aURL);
410 get: function TB_get(filter) {
411 var urls = this._storage.getURLs(filter);
415 _storage: this._storage,
416 getNext: function() {
417 var url = this._urls.shift();
418 return this._storage.retrieve(url);
420 hasMoreElements: function() {
421 return this._urls.length > 0;
428 /* Because sessionstore doesn't let us restore a single tab, we cut'n'paste
429 * a bunch of code here
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();
449 tab = tabbrowser.mCurrentTab;
452 loadInBackground = !loadInBackground;
455 tab = tabbrowser.loadOneTab('about:blank', null, null, null,
456 loadInBackground, false);
464 var browser = win.getBrowser().getBrowserForTab(tab);
465 var history = browser.webNavigation.sessionHistory;
467 if (history.count > 0) {
468 history.PurgeHistory(history.count);
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);
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;
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));
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;
501 browser.webNavigation.gotoIndex(activeIndex);
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);
514 _deserializeHistoryEntry: function TB__deserializeHistoryEntry(aEntry, aIdMap) {
515 var shEntry = Cc["@mozilla.org/browser/session-history-entry;1"].
516 createInstance(Ci.nsISHEntry);
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;
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;
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;
536 for (id = Date.now(); aIdMap.used[id]; id++);
537 aIdMap[aEntry.ID] = id;
538 aIdMap.used[id] = true;
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]);
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;
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);
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) {
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);
577 var event = document.createEvent("UIEvents");
578 event.initUIEvent("input", true, true, aContent, 0);
579 node.dispatchEvent(event);
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);
590 if (aData.scroll && /(\d+),(\d+)/.test(aData.scroll)) {
591 aContent.scrollTo(RegExp.$1, RegExp.$2);
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);
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;
606 restoreTextDataAndScrolling(content, this.__SS_restore_data, "");
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);
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;
620 getInterfaces: function TB_getInterfaces(countRef) {
621 var interfaces = [Ci.oyITaboo, Ci.nsIObserver, Ci.nsISupports];
622 countRef.value = interfaces.length;
625 getHelperForLanguage: function TB_getHelperForLanguage(language) {
628 contractID: TB_CONTRACTID,
629 classDescription: TB_CLASSNAME,
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))
639 throw Cr.NS_ERROR_NO_INTERFACE;
643 function GenericComponentFactory(ctor) {
647 GenericComponentFactory.prototype = {
652 createInstance: function(outer, iid) {
654 throw Cr.NS_ERROR_NO_AGGREGATION;
655 return (new this._ctor()).QueryInterface(iid);
659 QueryInterface: function(iid) {
660 if (iid.equals(Ci.nsIFactory) ||
661 iid.equals(Ci.nsISupports))
663 throw Cr.NS_ERROR_NO_INTERFACE;
668 QueryInterface: function(iid) {
669 if (iid.equals(Ci.nsIModule) ||
670 iid.equals(Ci.nsISupports))
673 throw Cr.NS_ERROR_NO_INTERFACE;
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;
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,
698 unregisterSelf: function(cm, location, type) {
699 var cr = cm.QueryInterface(Ci.nsIComponentRegistrar);
700 cr.unregisterFactoryLocation(TB_CLASSID, location);
703 canUnload: function(cm) {
708 function NSGetModule(compMgr, fileSpec)