Merge commit 'yosh/master'
[taboo.git] / components / oyTaboo.js
blob145e0129f0a02e7998f816866b3ee39a707c0b9b
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.
13 * Portions are derived from the Mozilla nsSessionStore component:
15 * Copyright (C) 2006 Simon Bünzli <zeniko@gmail.com>
17 * Contributor(s):
18 * Dietrich Ayala <autonome@gmail.com>
20 * Other portions derived from Firefox bookmarks code.
22 * Copyright (C) 1998 Netscape Communications Corporation.
24 * Contributor(s):
25 * Ben Goodger <ben@netscape.com> (Original Author)
26 * Joey Minta <jminta@gmail.com>
28 * Other portions derived from Flock favorites code.
30 * Copyright (C) 2005-2007 Flock Inc.
33 const TB_CONTRACTID = '@oy/taboo;1';
34 const TB_CLASSID = Components.ID('{962a9516-b177-4083-bbe8-e10f47cf8570}');
35 const TB_CLASSNAME = 'Taboo Service';
38 const Cc = Components.classes;
39 const Ci = Components.interfaces;
40 const Cr = Components.results;
41 const Cu = Components.utils;
43 /* from nspr's prio.h */
44 const PR_RDONLY = 0x01;
45 const PR_WRONLY = 0x02;
46 const PR_RDWR = 0x04;
47 const PR_CREATE_FILE = 0x08;
48 const PR_APPEND = 0x10;
49 const PR_TRUNCATE = 0x20;
50 const PR_SYNC = 0x40;
51 const PR_EXCL = 0x80;
53 const CAPABILITIES = [
54 "Subframes", "Plugins", "Javascript", "MetaRedirects", "Images"
57 const IMAGE_FULL_WIDTH = 500;
58 const IMAGE_FULL_HEIGHT = 500;
60 const IMAGE_THUMB_WIDTH = 125;
61 const IMAGE_THUMB_HEIGHT = 125;
64 function getObserverService() {
65 return Cc['@mozilla.org/observer-service;1']
66 .getService(Ci.nsIObserverService);
69 function getBoolPref(prefName, defaultValue) {
70 try {
71 var prefs = Cc['@mozilla.org/preferences-service;1']
72 .getService(Ci.nsIPrefBranch);
73 return prefs.getBoolPref(prefName);
75 catch (e) {
76 return defaultValue;
81 /* MD5 wrapper */
82 function hex_md5_stream(stream) {
83 var hasher = Components.classes["@mozilla.org/security/hash;1"]
84 .createInstance(Components.interfaces.nsICryptoHash);
85 hasher.init(hasher.MD5);
87 hasher.updateFromStream(stream, stream.available());
88 var hash = hasher.finish(false);
90 var ret = '';
91 for (var i = 0; i < hash.length; ++i) {
92 var hexChar = hash.charCodeAt(i).toString(16);
93 if (hexChar.length == 1)
94 ret += '0';
95 ret += hexChar;
98 return ret;
101 function hex_md5(s) {
102 var stream = Components.classes["@mozilla.org/io/string-input-stream;1"]
103 .createInstance(Components.interfaces.nsIStringInputStream);
104 stream.setData(s, s.length);
106 return hex_md5_stream(stream);
111 * Taboo Info Instance
114 function TabooInfo(url, title, description, favicon, imageURL, thumbURL,
115 created, updated, data) {
116 this.url = url;
117 this.title = title;
118 this.description = description;
119 this.favicon = favicon;
120 this.imageURL = imageURL;
121 this.thumbURL = thumbURL;
122 this.created = new Date(created);
123 this.updated = new Date(updated);
124 this.data = data;
127 TabooInfo.prototype = {
128 QueryInterface: function(iid) {
129 if (!iid.equals(Ci.nsISupports) &&
130 !iid.equals(Ci.oyITabooInfo)) {
131 throw Cr.NS_ERROR_NO_INTERFACE;
133 return this;
138 * Taboo Service Component
142 function snapshot(win, outputWidth, outputHeight) {
143 var content = win.content;
145 var canvas = win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
147 var realW = content.document.body ? content.document.body.clientWidth
148 : content.innerWidth;
149 var realH = content.innerHeight;
151 var pW = outputWidth * 1.0 / realW;
152 var pH = outputHeight * 1.0 / realH;
154 var p = pW;
156 if (pH < pW) {
157 p = pH;
160 var w = p * realW;
161 var h = p * realH;
163 canvas.setAttribute("width", Math.floor(w));
164 canvas.setAttribute("height", Math.floor(h));
166 var ctx = canvas.getContext("2d");
167 ctx.scale(p, p);
168 ctx.drawWindow(content, content.scrollX, content.scrollY, realW, realH, "rgb(0,0,0)");
170 var imageData = canvas.toDataURL();
171 return win.atob(imageData.substr('data:image/png;base64,'.length));
175 function TabooStorageSQL() {
176 this._tabooDir = Cc['@mozilla.org/file/directory_service;1']
177 .getService(Ci.nsIProperties).get('ProfD', Ci.nsILocalFile);
178 this._tabooDir.append('taboo');
180 if (!this._tabooDir.exists())
181 this._tabooDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0700);
183 var dbfile = this._tabooDir.clone();
184 dbfile.append('taboo.sqlite');
186 var DB = loadSubScript('chrome://taboo/content/sqlite.js').DB;
187 this._db = new DB(dbfile);
189 this._db.Table('taboo_data', {
190 url : 'TEXT PRIMARY KEY',
191 title : 'TEXT',
192 description : 'TEXT',
193 md5 : 'TEXT',
194 favicon : 'TEXT',
195 full : 'TEXT',
196 created : 'INTEGER',
197 updated : 'INTEGER',
198 deleted : 'INTEGER'
201 this._store = this._db.taboo_data;
204 TabooStorageSQL.prototype = {
205 save: function TSSQL_save(url, description, data, fullImage, thumbImage) {
206 var title = data.entries[data.index - 1].title;
208 if (!title) {
209 var ios = Cc['@mozilla.org/network/io-service;1']
210 .getService(Ci.nsIIOService);
211 var uri = ios.newURI(url, null, null);
213 if (uri.path.length > 1) {
214 var parts = uri.path.split('/');
215 while (!title && parts.length)
216 title = parts.pop();
219 if (!title)
220 title = uri.host;
223 var updated = Date.now();
225 var entry = this._store.find(url);
227 if (!entry) {
228 entry = this._store.new();
229 entry.url = url;
230 entry.md5 = hex_md5(url);
231 entry.created = updated;
234 if (description) {
235 entry.description = description;
238 entry.title = title;
239 entry.updated = updated;
240 entry.deleted = null;
241 entry.full = data.toSource();
243 entry.save();
245 this._saveImage(fullImage, this._getImageFile(entry.md5));
246 this._saveImage(thumbImage, this._getThumbFile(entry.md5));
248 saveFavicon: function TSSQL_saveFavicon(url, favicon) {
249 var entry = this._store.find(url);
250 if (entry) {
251 entry.favicon = favicon;
252 entry.save();
255 exists: function TSSQL_exists(url) {
256 return Boolean(this._store.find(url));
258 delete: function TSSQL_delete(url) {
259 this._deleteOp(url, Date.now());
261 undelete: function TSSQL_undelete(url) {
262 this._deleteOp(url, null);
264 reallyDelete: function TSSQL_reallyDelete(url) {
265 var entry = this._store.find(url);
266 if (entry) {
267 entry.destroy();
270 try {
271 var file, md5 = hex_md5(url);
273 file = this._getImageFile(md5);
274 file.remove(false);
276 file = this._getThumbFile(md5);
277 file.remove(false);
279 catch (e) { }
281 retrieve: function TSSQL_retrieve(url) {
282 var entry = this._store.find(url);
283 if (!entry)
284 return null;
286 var ios = Cc['@mozilla.org/network/io-service;1']
287 .getService(Ci.nsIIOService);
288 var fileHandler = ios.getProtocolHandler('file')
289 .QueryInterface(Ci.nsIFileProtocolHandler);
291 var imageFile = this._getImageFile(entry.md5);
292 var imageURL = fileHandler.getURLSpecFromFile(imageFile);
294 var thumbFile = this._getThumbFile(entry.md5);
295 var thumbURL;
296 if (thumbFile.exists()) {
297 thumbURL = fileHandler.getURLSpecFromFile(thumbFile);
298 } else {
299 thumbURL = imageURL;
302 var data = entry.full.replace(/\r\n?/g, '\n');
303 var sandbox = new Cu.Sandbox('about:blank');
304 var state = Cu.evalInSandbox(data, sandbox);
306 return new TabooInfo(url, entry.title, entry.description, entry.favicon,
307 imageURL, thumbURL, entry.created, entry.updated,
308 state);
310 getURLs: function TSSQL_getURLs(filter, deleted) {
311 var condition = [];
313 var sortkey, sql = '';
315 if (filter) {
316 sql += '(url LIKE ?1 or title LIKE ?1 or description LIKE ?1) and ';
317 // TODO: escape %'s before passing in
318 condition.push('%' + filter + '%');
321 if (deleted) {
322 sql += 'deleted IS NOT NULL';
323 sortkey = 'deleted DESC';
324 } else {
325 sql += 'deleted IS NULL';
326 sortkey = 'updated DESC';
329 condition.unshift(sql);
331 var results = this._store.find(condition, sortkey);
332 return results.map(function(entry) { return entry.url });
334 _getImageFile: function TSSQL__getImageFile(id) {
335 var file = this._tabooDir.clone();
336 file.append(id + '.png');
337 return file;
339 _getThumbFile: function TSSQL__getPreviewFile(id) {
340 var file = this._tabooDir.clone();
341 file.append(id + '-' + IMAGE_THUMB_WIDTH + '.png');
342 return file;
344 _saveImage: function TSSQL__saveImage(imageData, file) {
345 try {
346 var ostream = Cc['@mozilla.org/network/file-output-stream;1']
347 .createInstance(Ci.nsIFileOutputStream);
348 ostream.init(file, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, 0600, 0);
350 ostream.write(imageData, imageData.length);
351 ostream.close();
353 catch (e) { }
355 _deleteOp: function TSSQL__deleteOp(url, deleted) {
356 var entry = this._store.find(url);
357 if (entry) {
358 entry.deleted = deleted;
359 entry.save();
365 function TabooService() {
366 var obs = getObserverService();
367 obs.addObserver(this, 'profile-after-change', false);
370 TabooService.prototype = {
371 _init: function TB__init() {
372 this._storage = new TabooStorageSQL();
374 observe: function TB_observe(subject, topic, state) {
375 var obs = getObserverService();
377 switch (topic) {
378 case 'profile-after-change':
379 obs.removeObserver(this, 'profile-after-change');
380 this._init();
381 break;
385 save: function TB_save(aDescription) {
386 var wm = Cc["@mozilla.org/appshell/window-mediator;1"]
387 .getService(Ci.nsIWindowMediator);
388 var win = wm.getMostRecentWindow('navigator:browser');
390 var tabbrowser = win.getBrowser();
391 var selectedBrowser = tabbrowser.selectedBrowser;
392 var selectedTab = tabbrowser.selectedTab;
394 var currentTab = -1;
395 var browsers = tabbrowser.browsers;
396 for (var i = 0; i < browsers.length; i++) {
397 if (browsers[i] == selectedBrowser)
398 currentTab = i;
401 if (currentTab == -1)
402 return false;
404 var ss = Cc['@mozilla.org/browser/sessionstore;1']
405 .getService(Ci.nsISessionStore);
406 var winJSON = "(" + ss.getWindowState(win) + ")";
408 if (getBoolPref('extensions.taboo.debug', false))
409 dump(winJSON + "\n");
411 var sandbox = new Cu.Sandbox('about:blank');
412 var winState = Cu.evalInSandbox(winJSON, sandbox);
414 var state = winState.windows[0].tabs[currentTab];
416 var url = state.entries[state.index - 1].url;
417 url = url.replace(/#.*$/, '');
419 var fullImage = snapshot(win, IMAGE_FULL_WIDTH, IMAGE_FULL_HEIGHT);
420 var thumbImage = snapshot(win, IMAGE_THUMB_WIDTH, IMAGE_THUMB_HEIGHT);
422 this._storage.save(url, aDescription, state, fullImage, thumbImage);
424 var faviconURL = selectedTab.getAttribute('image');
425 if (faviconURL) {
426 var ios = Cc['@mozilla.org/network/io-service;1']
427 .getService(Ci.nsIIOService);
428 var chan = ios.newChannel(faviconURL, null, null);
429 var listener = new tabooFavIconLoadListener(url, faviconURL, chan,
430 this._storage);
431 chan.notificationCallbacks = listener;
432 chan.asyncOpen(listener, null);
435 return true;
437 isSaved: function TB_isSaved(aURL) {
438 return this._storage.exists(aURL);
440 delete: function TB_delete(aURL) {
441 this._storage.delete(aURL);
443 undelete: function TB_undelete(aURL) {
444 this._storage.undelete(aURL);
446 reallyDelete: function TB_reallyDelete(aURL) {
447 this._storage.reallyDelete(aURL);
449 get: function TB_get(filter, deleted) {
450 var urls = this._storage.getURLs(filter, deleted);
452 var enumerator = {
453 _urls: urls,
454 _storage: this._storage,
455 getNext: function() {
456 var url = this._urls.shift();
457 return this._storage.retrieve(url);
459 hasMoreElements: function() {
460 return this._urls.length > 0;
464 return enumerator;
467 /* Because sessionstore doesn't let us restore a single tab, we cut'n'paste
468 * a bunch of code here
470 open: function TB_open(aURL, aWhere) {
471 var info = this._storage.retrieve(aURL);
472 var tabData = info.data;
474 // helper hash for ensuring unique frame IDs
475 var idMap = { used: {} };
477 var wm = Cc['@mozilla.org/appshell/window-mediator;1']
478 .getService(Ci.nsIWindowMediator);
479 var win = wm.getMostRecentWindow('navigator:browser');
481 var loadInBackground = getBoolPref("browser.tabs.loadBookmarksInBackground", false);
483 var tabbrowser = win.getBrowser();
485 var tab;
486 switch (aWhere) {
487 case 'current':
488 tab = tabbrowser.mCurrentTab;
489 break;
490 case 'tabshifted':
491 loadInBackground = !loadInBackground;
492 // fall through
493 case 'tab':
494 tab = tabbrowser.loadOneTab('about:blank', null, null, null,
495 loadInBackground, false);
496 break;
497 default:
498 return;
501 var _this = this;
503 var browser = win.getBrowser().getBrowserForTab(tab);
504 var history = browser.webNavigation.sessionHistory;
506 if (history.count > 0) {
507 history.PurgeHistory(history.count);
509 history.QueryInterface(Ci.nsISHistoryInternal);
511 browser.markupDocumentViewer.textZoom = parseFloat(tabData.zoom || 1);
513 for (var i = 0; i < tabData.entries.length; i++) {
514 history.addEntry(this._deserializeHistoryEntry(tabData.entries[i], idMap), true);
517 // make sure to reset the capabilities and attributes, in case this tab gets reused
518 var disallow = (tabData.disallow)?tabData.disallow.split(","):[];
519 CAPABILITIES.forEach(function(aCapability) {
520 browser.docShell["allow" + aCapability] = disallow.indexOf(aCapability) == -1;
522 Array.filter(tab.attributes, function(aAttr) {
523 return (_this.xulAttributes.indexOf(aAttr.name) > -1);
524 }).forEach(tab.removeAttribute, tab);
525 if (tabData.xultab) {
526 tabData.xultab.split(" ").forEach(function(aAttr) {
527 if (/^([^\s=]+)=(.*)/.test(aAttr)) {
528 tab.setAttribute(RegExp.$1, decodeURI(RegExp.$2));
533 // notify the tabbrowser that the tab chrome has been restored
534 var event = win.document.createEvent("Events");
535 event.initEvent("SSTabRestoring", true, false);
536 tab.dispatchEvent(event);
538 var activeIndex = (tabData.index || tabData.entries.length) - 1;
539 try {
540 browser.webNavigation.gotoIndex(activeIndex);
542 catch (ex) { } // ignore an invalid tabData.index
544 // restore those aspects of the currently active documents
545 // which are not preserved in the plain history entries
546 // (mainly scroll state and text data)
547 browser.__SS_restore_data = tabData.entries[activeIndex] || {};
548 browser.__SS_restore_text = tabData.text || "";
549 browser.__SS_restore_tab = tab;
550 browser.__SS_restore = this.restoreDocument_proxy;
551 browser.addEventListener("load", browser.__SS_restore, true);
553 _deserializeHistoryEntry: function TB__deserializeHistoryEntry(aEntry, aIdMap) {
554 var shEntry = Cc["@mozilla.org/browser/session-history-entry;1"].
555 createInstance(Ci.nsISHEntry);
557 var ioService = Cc["@mozilla.org/network/io-service;1"].
558 getService(Ci.nsIIOService);
559 shEntry.setURI(ioService.newURI(aEntry.url, null, null));
560 shEntry.setTitle(aEntry.title || aEntry.url);
561 shEntry.setIsSubFrame(aEntry.subframe || false);
562 shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory;
564 if (aEntry.cacheKey) {
565 var cacheKey = Cc["@mozilla.org/supports-PRUint32;1"].
566 createInstance(Ci.nsISupportsPRUint32);
567 cacheKey.data = aEntry.cacheKey;
568 shEntry.cacheKey = cacheKey;
570 if (aEntry.ID) {
571 // get a new unique ID for this frame (since the one from the last
572 // start might already be in use)
573 var id = aIdMap[aEntry.ID] || 0;
574 if (!id) {
575 for (id = Date.now(); aIdMap.used[id]; id++);
576 aIdMap[aEntry.ID] = id;
577 aIdMap.used[id] = true;
579 shEntry.ID = id;
582 var scrollPos = (aEntry.scroll || "0,0").split(",");
583 scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0];
584 shEntry.setScrollPosition(scrollPos[0], scrollPos[1]);
586 if (aEntry.postdata) {
587 var stream = Cc["@mozilla.org/io/string-input-stream;1"].
588 createInstance(Ci.nsIStringInputStream);
589 stream.setData(aEntry.postdata, -1);
590 shEntry.postData = stream;
593 if (aEntry.children && shEntry instanceof Ci.nsISHContainer) {
594 for (var i = 0; i < aEntry.children.length; i++) {
595 shEntry.AddChild(this._deserializeHistoryEntry(aEntry.children[i], aIdMap), i);
599 return shEntry;
601 restoreDocument_proxy: function TB_restoreDocument_proxy(aEvent) {
602 // wait for the top frame to be loaded completely
603 if (!aEvent || !aEvent.originalTarget || !aEvent.originalTarget.defaultView || aEvent.originalTarget.defaultView != aEvent.originalTarget.defaultView.top) {
604 return;
607 var textArray = this.__SS_restore_text ? this.__SS_restore_text.split(" ") : [];
608 function restoreTextData(aContent, aPrefix) {
609 textArray.forEach(function(aEntry) {
610 if (/^((?:\d+\|)*)(#?)([^\s=]+)=(.*)$/.test(aEntry) && (!RegExp.$1 || RegExp.$1 == aPrefix)) {
611 var document = aContent.document;
612 var node = RegExp.$2 ? document.getElementById(RegExp.$3) : document.getElementsByName(RegExp.$3)[0] || null;
613 if (node && "value" in node) {
614 node.value = decodeURI(RegExp.$4);
616 var event = document.createEvent("UIEvents");
617 event.initUIEvent("input", true, true, aContent, 0);
618 node.dispatchEvent(event);
624 function restoreTextDataAndScrolling(aContent, aData, aPrefix) {
625 restoreTextData(aContent, aPrefix);
626 if (aData.innerHTML) {
627 aContent.setTimeout(function(aHTML) { if (this.document.designMode == "on") { this.document.body.innerHTML = aHTML; } }, 0, aData.innerHTML);
629 if (aData.scroll && /(\d+),(\d+)/.test(aData.scroll)) {
630 aContent.scrollTo(RegExp.$1, RegExp.$2);
632 for (var i = 0; i < aContent.frames.length; i++) {
633 if (aData.children && aData.children[i]) {
634 restoreTextDataAndScrolling(aContent.frames[i], aData.children[i], i + "|" + aPrefix);
639 var content = XPCNativeWrapper(aEvent.originalTarget).defaultView;
640 if (this.currentURI.spec == "about:config") {
641 // unwrap the document for about:config because otherwise the properties
642 // of the XBL bindings - as the textbox - aren't accessible (see bug 350718)
643 content = content.wrappedJSObject;
645 restoreTextDataAndScrolling(content, this.__SS_restore_data, "");
647 // notify the tabbrowser that this document has been completely restored
648 var event = this.ownerDocument.createEvent("Events");
649 event.initEvent("SSTabRestored", true, false);
650 this.__SS_restore_tab.dispatchEvent(event);
652 this.removeEventListener("load", this.__SS_restore, true);
653 delete this.__SS_restore_data;
654 delete this.__SS_restore_text;
655 delete this.__SS_restore_tab;
657 xulAttributes: [],
659 getInterfaces: function TB_getInterfaces(countRef) {
660 var interfaces = [Ci.oyITaboo, Ci.nsIObserver, Ci.nsISupports];
661 countRef.value = interfaces.length;
662 return interfaces;
664 getHelperForLanguage: function TB_getHelperForLanguage(language) {
665 return null;
667 contractID: TB_CONTRACTID,
668 classDescription: TB_CLASSNAME,
669 classID: TB_CLASSID,
670 implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,
671 flags: Ci.nsIClassInfo.SINGLETON,
673 QueryInterface: function TB_QueryInterface(iid) {
674 if (iid.equals(Ci.oyITaboo) ||
675 iid.equals(Ci.nsIObserver) ||
676 iid.equals(Ci.nsISupports))
677 return this;
678 throw Cr.NS_ERROR_NO_INTERFACE;
683 /* This is swiped from bookmarks.js in Firefox. In Firefox 3, this *should*
684 * be easier, and not require cut'n'pasting
686 function tabooFavIconLoadListener(url, faviconurl, channel, storage) {
687 this.mURL = url;
688 this.mFavIconURL = faviconurl;
689 this.mCountRead = 0;
690 this.mChannel = channel;
691 this.mStorage = storage;
694 tabooFavIconLoadListener.prototype = {
695 mURL : null,
696 mFavIconURL : null,
697 mCountRead : null,
698 mChannel : null,
699 mBytes : Array(),
700 mStream : null,
702 QueryInterface: function (iid) {
703 if (!iid.equals(Components.interfaces.nsISupports) &&
704 !iid.equals(Components.interfaces.nsIInterfaceRequestor) &&
705 !iid.equals(Components.interfaces.nsIRequestObserver) &&
706 !iid.equals(Components.interfaces.nsIChannelEventSink) &&
707 !iid.equals(Components.interfaces.nsIProgressEventSink) && // see below
708 !iid.equals(Components.interfaces.nsIStreamListener)) {
709 throw Components.results.NS_ERROR_NO_INTERFACE;
711 return this;
714 // nsIInterfaceRequestor
715 getInterface: function (iid) {
716 try {
717 return this.QueryInterface(iid);
718 } catch (e) {
719 throw Components.results.NS_NOINTERFACE;
723 // nsIRequestObserver
724 onStartRequest : function (aRequest, aContext) {
725 this.mStream = Components.classes['@mozilla.org/binaryinputstream;1'].createInstance(Components.interfaces.nsIBinaryInputStream);
728 onStopRequest : function (aRequest, aContext, aStatusCode) {
729 var httpChannel = this.mChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
730 if ((httpChannel && httpChannel.requestSucceeded) &&
731 Components.isSuccessCode(aStatusCode) &&
732 this.mCountRead > 0)
734 var dataurl;
735 // XXX - arbitrary size beyond which we won't store a favicon. This is /extremely/
736 // generous, and is probably too high.
737 if (this.mCountRead > 16384) {
738 dataurl = "data:"; // hack meaning "pretend this doesn't exist"
739 } else {
740 // get us a mime type for this
741 var mimeType = null;
743 const nsICategoryManager = Components.interfaces.nsICategoryManager;
744 const nsIContentSniffer = Components.interfaces.nsIContentSniffer;
746 var catMgr = Components.classes["@mozilla.org/categorymanager;1"].getService(nsICategoryManager);
747 var sniffers = catMgr.enumerateCategory("content-sniffing-services");
748 while (mimeType == null && sniffers.hasMoreElements()) {
749 var snifferCID = sniffers.getNext().QueryInterface(Components.interfaces.nsISupportsCString).toString();
750 var sniffer = Components.classes[snifferCID].getService(nsIContentSniffer);
752 try {
753 mimeType = sniffer.getMIMETypeFromContent (this.mBytes, this.mCountRead);
754 } catch (e) {
755 mimeType = null;
756 // ignore
761 if (this.mBytes && this.mCountRead > 0 && mimeType != null) {
762 var data = 'data:';
763 data += mimeType;
764 data += ';base64;';
766 var iconData = String.fromCharCode.apply(null, this.mBytes);
767 data += base64Encode(iconData);
769 this.mStorage.saveFavicon(this.mURL, data);
773 this.mChannel = null;
776 // nsIStreamObserver
777 onDataAvailable : function (aRequest, aContext, aInputStream, aOffset, aCount) {
778 // we could get a different aInputStream, so we don't save this;
779 // it's unlikely we'll get more than one onDataAvailable for a
780 // favicon anyway
781 this.mStream.setInputStream(aInputStream);
783 var chunk = this.mStream.readByteArray(aCount);
784 this.mBytes = this.mBytes.concat(chunk);
785 this.mCountRead += aCount;
788 // nsIChannelEventSink
789 onChannelRedirect : function (aOldChannel, aNewChannel, aFlags) {
790 this.mChannel = aNewChannel;
793 // nsIProgressEventSink: the only reason we support
794 // nsIProgressEventSink is to shut up a whole slew of xpconnect
795 // warnings in debug builds. (see bug #253127)
796 onProgress : function (aRequest, aContext, aProgress, aProgressMax) { },
797 onStatus : function (aRequest, aContext, aStatus, aStatusArg) { }
800 // From flockFavoritesService.js
801 function base64Encode(aInput) {
802 var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
803 var output = "";
804 while (aInput.length > 0) {
805 output += chars[aInput.charCodeAt(0) >> 2];
806 output += chars[((aInput.charCodeAt(0) & 0x03) << 4) |
807 (aInput.length > 1 ? ((aInput.charCodeAt(1) & 0xF0) >> 4) : 0)];
808 output += chars[aInput.length > 1 ?
809 ((aInput.charCodeAt(1) & 0x0F) << 2) |
810 (aInput.length > 2 ? ((aInput.charCodeAt(2) & 0xC0) >> 6) : 0) : 64];
811 output += chars[aInput.length > 2 ?
812 (aInput.charCodeAt(2) & 0x3F) : 64];
813 if (aInput.length > 3) {
814 aInput = aInput.substr(3);
815 } else {
816 break;
819 return output;
823 function GenericComponentFactory(ctor) {
824 this._ctor = ctor;
827 GenericComponentFactory.prototype = {
829 _ctor: null,
831 // nsIFactory
832 createInstance: function(outer, iid) {
833 if (outer != null)
834 throw Cr.NS_ERROR_NO_AGGREGATION;
835 return (new this._ctor()).QueryInterface(iid);
838 // nsISupports
839 QueryInterface: function(iid) {
840 if (iid.equals(Ci.nsIFactory) ||
841 iid.equals(Ci.nsISupports))
842 return this;
843 throw Cr.NS_ERROR_NO_INTERFACE;
847 var Module = {
848 QueryInterface: function(iid) {
849 if (iid.equals(Ci.nsIModule) ||
850 iid.equals(Ci.nsISupports))
851 return this;
853 throw Cr.NS_ERROR_NO_INTERFACE;
856 getClassObject: function(cm, cid, iid) {
857 if (!iid.equals(Ci.nsIFactory))
858 throw Cr.NS_ERROR_NOT_IMPLEMENTED;
860 if (cid.equals(TB_CLASSID))
861 return new GenericComponentFactory(TabooService)
863 throw Cr.NS_ERROR_NO_INTERFACE;
866 registerSelf: function(cm, file, location, type) {
867 var cr = cm.QueryInterface(Ci.nsIComponentRegistrar);
868 cr.registerFactoryLocation(TB_CLASSID, TB_CLASSNAME, TB_CONTRACTID,
869 file, location, type);
871 var catman = Cc['@mozilla.org/categorymanager;1']
872 .getService(Ci.nsICategoryManager);
873 catman.addCategoryEntry('app-startup', TB_CLASSNAME,
874 'service,' + TB_CONTRACTID,
875 true, true);
878 unregisterSelf: function(cm, location, type) {
879 var cr = cm.QueryInterface(Ci.nsIComponentRegistrar);
880 cr.unregisterFactoryLocation(TB_CLASSID, location);
883 canUnload: function(cm) {
884 return true;
888 function NSGetModule(compMgr, fileSpec)
890 return Module;
894 function loadSubScript(spec) {
895 var loader = Cc['@mozilla.org/moz/jssubscript-loader;1']
896 .getService(Ci.mozIJSSubScriptLoader);
897 var context = {};
898 loader.loadSubScript(spec, context);
899 return context;