summaryrefslogtreecommitdiff
path: root/browser/components/feeds/WebContentConverter.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/feeds/WebContentConverter.js')
-rw-r--r--browser/components/feeds/WebContentConverter.js927
1 files changed, 927 insertions, 0 deletions
diff --git a/browser/components/feeds/WebContentConverter.js b/browser/components/feeds/WebContentConverter.js
new file mode 100644
index 000000000..a6b144c65
--- /dev/null
+++ b/browser/components/feeds/WebContentConverter.js
@@ -0,0 +1,927 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+function LOG(str) {
+ dump("*** " + str + "\n");
+}
+
+const WCCR_CONTRACTID = "@mozilla.org/embeddor.implemented/web-content-handler-registrar;1";
+const WCCR_CLASSID = Components.ID("{792a7e82-06a0-437c-af63-b2d12e808acc}");
+
+const WCC_CLASSID = Components.ID("{db7ebf28-cc40-415f-8a51-1b111851df1e}");
+const WCC_CLASSNAME = "Web Service Handler";
+
+const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed";
+const TYPE_ANY = "*/*";
+const TYPE_BLACKLIST = [
+ "application/x-www-form-urlencoded",
+ "application/xhtml+xml",
+ "application/xml",
+ "application/mathml+xml",
+ "application/xslt+xml",
+ "application/x-xpinstall",
+ "image/gif",
+ "image/jpg",
+ "image/jpeg",
+ "image/png",
+ "image/x-png",
+ "image/webp",
+#ifdef MOZ_JXR
+ "image/jxr",
+ "image/vnd.ms-photo",
+#endif
+ "image/svg+xml",
+ "image/bmp",
+ "image/x-ms-bmp",
+ "image/icon",
+ "image/x-icon",
+ "image/vnd.microsoft.icon",
+ "multipart/x-mixed-replace",
+ "multipart/form-data",
+ "text/cache-manifest",
+ "text/css",
+ "text/xsl",
+ "text/html",
+ "text/ping",
+ "text/plain",
+ "text/xml",
+ "text/javascript", // To prevent malicious intent blocking scripting.
+ "text/ecmascript"];
+
+const PREF_CONTENTHANDLERS_AUTO = "browser.contentHandlers.auto.";
+const PREF_CONTENTHANDLERS_BRANCH = "browser.contentHandlers.types.";
+const PREF_SELECTED_WEB = "browser.feeds.handlers.webservice";
+const PREF_SELECTED_ACTION = "browser.feeds.handler";
+const PREF_SELECTED_READER = "browser.feeds.handler.default";
+const PREF_HANDLER_EXTERNAL_PREFIX = "network.protocol-handler.external";
+const PREF_ALLOW_DIFFERENT_HOST = "gecko.handlerService.allowRegisterFromDifferentHost";
+
+const STRING_BUNDLE_URI = "chrome://browser/locale/feeds/subscribe.properties";
+
+const NS_ERROR_MODULE_DOM = 2152923136;
+const NS_ERROR_DOM_SYNTAX_ERR = NS_ERROR_MODULE_DOM + 12;
+
+function WebContentConverter() {
+}
+WebContentConverter.prototype = {
+ convert: function() { },
+ asyncConvertData: function() { },
+ onDataAvailable: function() { },
+ onStopRequest: function() { },
+
+ onStartRequest: function(request, context) {
+ var wccr =
+ Cc[WCCR_CONTRACTID].
+ getService(Ci.nsIWebContentConverterService);
+ wccr.loadPreferredHandler(request);
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIStreamConverter) ||
+ iid.equals(Ci.nsIStreamListener) ||
+ iid.equals(Ci.nsISupports))
+ return this;
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+};
+
+var WebContentConverterFactory = {
+ createInstance: function(outer, iid) {
+ if (outer != null)
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ return new WebContentConverter().QueryInterface(iid);
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIFactory) ||
+ iid.equals(Ci.nsISupports))
+ return this;
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+};
+
+function ServiceInfo(contentType, uri, name) {
+ this._contentType = contentType;
+ this._uri = uri;
+ this._name = name;
+}
+ServiceInfo.prototype = {
+ /**
+ * See nsIHandlerApp
+ */
+ get name() {
+ return this._name;
+ },
+
+ /**
+ * See nsIHandlerApp
+ */
+ equals: function(aHandlerApp) {
+ if (!aHandlerApp)
+ throw Cr.NS_ERROR_NULL_POINTER;
+
+ if (aHandlerApp instanceof Ci.nsIWebContentHandlerInfo &&
+ aHandlerApp.contentType == this.contentType &&
+ aHandlerApp.uri == this.uri)
+ return true;
+
+ return false;
+ },
+
+ /**
+ * See nsIWebContentHandlerInfo
+ */
+ get contentType() {
+ return this._contentType;
+ },
+
+ /**
+ * See nsIWebContentHandlerInfo
+ */
+ get uri() {
+ return this._uri;
+ },
+
+ /**
+ * See nsIWebContentHandlerInfo
+ */
+ getHandlerURI: function(uri) {
+ return this._uri.replace(/%s/gi, encodeURIComponent(uri));
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIWebContentHandlerInfo) ||
+ iid.equals(Ci.nsISupports))
+ return this;
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+};
+
+function WebContentConverterRegistrar() {
+ this._contentTypes = { };
+ this._autoHandleContentTypes = { };
+}
+
+WebContentConverterRegistrar.prototype = {
+ get stringBundle() {
+ var sb = Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService).
+ createBundle(STRING_BUNDLE_URI);
+ delete WebContentConverterRegistrar.prototype.stringBundle;
+ return WebContentConverterRegistrar.prototype.stringBundle = sb;
+ },
+
+ _getFormattedString: function(key, params) {
+ return this.stringBundle.formatStringFromName(key, params, params.length);
+ },
+
+ _getString: function(key) {
+ return this.stringBundle.GetStringFromName(key);
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ getAutoHandler:
+ function(contentType) {
+ contentType = this._resolveContentType(contentType);
+ if (contentType in this._autoHandleContentTypes)
+ return this._autoHandleContentTypes[contentType];
+ return null;
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ setAutoHandler:
+ function(contentType, handler) {
+ if (handler && !this._typeIsRegistered(contentType, handler.uri))
+ throw Cr.NS_ERROR_NOT_AVAILABLE;
+
+ contentType = this._resolveContentType(contentType);
+ this._setAutoHandler(contentType, handler);
+
+ var ps =
+ Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefService);
+ var autoBranch = ps.getBranch(PREF_CONTENTHANDLERS_AUTO);
+ if (handler)
+ autoBranch.setCharPref(contentType, handler.uri);
+ else if (autoBranch.prefHasUserValue(contentType))
+ autoBranch.clearUserPref(contentType);
+
+ ps.savePrefFile(null);
+ },
+
+ /**
+ * Update the internal data structure (not persistent)
+ */
+ _setAutoHandler:
+ function(contentType, handler) {
+ if (handler)
+ this._autoHandleContentTypes[contentType] = handler;
+ else if (contentType in this._autoHandleContentTypes)
+ delete this._autoHandleContentTypes[contentType];
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ getWebContentHandlerByURI:
+ function(contentType, uri) {
+ var handlers = this.getContentHandlers(contentType, { });
+ for (var i = 0; i < handlers.length; ++i) {
+ if (handlers[i].uri == uri)
+ return handlers[i];
+ }
+ return null;
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ loadPreferredHandler:
+ function(request) {
+ var channel = request.QueryInterface(Ci.nsIChannel);
+ var contentType = this._resolveContentType(channel.contentType);
+ var handler = this.getAutoHandler(contentType);
+ if (handler) {
+ request.cancel(Cr.NS_ERROR_FAILURE);
+
+ var webNavigation =
+ channel.notificationCallbacks.getInterface(Ci.nsIWebNavigation);
+ webNavigation.loadURI(handler.getHandlerURI(channel.URI.spec),
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
+ null, null, null);
+ }
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ removeProtocolHandler:
+ function(aProtocol, aURITemplate) {
+ var eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
+ getService(Ci.nsIExternalProtocolService);
+ var handlerInfo = eps.getProtocolHandlerInfo(aProtocol);
+ var handlers = handlerInfo.possibleApplicationHandlers;
+ for (let i = 0; i < handlers.length; i++) {
+ try { // We only want to test web handlers
+ let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp);
+ if (handler.uriTemplate == aURITemplate) {
+ handlers.removeElementAt(i);
+ var hs = Cc["@mozilla.org/uriloader/handler-service;1"].
+ getService(Ci.nsIHandlerService);
+ hs.store(handlerInfo);
+ return;
+ }
+ } catch (e) { /* it wasn't a web handler */ }
+ }
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ removeContentHandler:
+ function(contentType, uri) {
+ function notURI(serviceInfo) {
+ return serviceInfo.uri != uri;
+ }
+
+ if (contentType in this._contentTypes) {
+ this._contentTypes[contentType] =
+ this._contentTypes[contentType].filter(notURI);
+ }
+ },
+
+ /**
+ *
+ */
+ _mappings: {
+ "application/rss+xml": TYPE_MAYBE_FEED,
+ "application/atom+xml": TYPE_MAYBE_FEED,
+ },
+
+ /**
+ * These are types for which there is a separate content converter aside
+ * from our built in generic one. We should not automatically register
+ * a factory for creating a converter for these types.
+ */
+ _blockedTypes: {
+ "application/vnd.mozilla.maybe.feed": true,
+ },
+
+ /**
+ * Determines the "internal" content type based on the _mappings.
+ * @param contentType
+ * @returns The resolved contentType value.
+ */
+ _resolveContentType:
+ function(contentType) {
+ if (contentType in this._mappings)
+ return this._mappings[contentType];
+ return contentType;
+ },
+
+ _makeURI: function(aURL, aOriginCharset, aBaseURI) {
+ var ioService = Components.classes["@mozilla.org/network/io-service;1"]
+ .getService(Components.interfaces.nsIIOService);
+ return ioService.newURI(aURL, aOriginCharset, aBaseURI);
+ },
+
+ _checkAndGetURI:
+ function(aURIString, aContentWindow)
+ {
+ try {
+ let baseURI = aContentWindow.document.baseURIObject;
+ var uri = this._makeURI(aURIString, null, baseURI);
+ } catch (ex) {
+ // not supposed to throw according to spec
+ return;
+ }
+
+ // For security reasons we reject non-http(s) urls (see bug 354316),
+ // we may need to revise this once we support more content types
+ // XXX this should be a "security exception" according to spec, but that
+ // isn't defined yet.
+ if (uri.scheme != "http" && uri.scheme != "https")
+ throw("Permission denied to add " + uri.spec + " as a content or protocol handler");
+
+ // We also reject handlers registered from a different host (see bug 402287)
+ // The pref allows us to test the feature
+ var pb = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+ if (!pb.getBoolPref(PREF_ALLOW_DIFFERENT_HOST) &&
+ (!["http:", "https:"].includes(aContentWindow.location.protocol) ||
+ aContentWindow.location.hostname != uri.host)) {
+ throw("Permission denied to add " + uri.spec + " as a content or protocol handler");
+ }
+
+ // If the uri doesn't contain '%s', it won't be a good handler
+ if (uri.spec.indexOf("%s") < 0)
+ throw NS_ERROR_DOM_SYNTAX_ERR;
+
+ return uri;
+ },
+
+ /**
+ * Determines if a web handler is already registered.
+ *
+ * @param aProtocol
+ * The scheme of the web handler we are checking for.
+ * @param aURITemplate
+ * The URI template that the handler uses to handle the protocol.
+ * @return true if it is already registered, false otherwise.
+ */
+ _protocolHandlerRegistered:
+ function(aProtocol, aURITemplate) {
+ var eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
+ getService(Ci.nsIExternalProtocolService);
+ var handlerInfo = eps.getProtocolHandlerInfo(aProtocol);
+ var handlers = handlerInfo.possibleApplicationHandlers;
+ for (let i = 0; i < handlers.length; i++) {
+ try { // We only want to test web handlers
+ let handler = handlers.queryElementAt(i, Ci.nsIWebHandlerApp);
+ if (handler.uriTemplate == aURITemplate)
+ return true;
+ } catch (e) { /* it wasn't a web handler */ }
+ }
+ return false;
+ },
+
+ /**
+ * See nsIWebContentHandlerRegistrar
+ */
+ registerProtocolHandler:
+ function(aProtocol, aURIString, aTitle, aContentWindow) {
+ LOG("registerProtocolHandler(" + aProtocol + "," + aURIString + "," + aTitle + ")");
+
+ var uri = this._checkAndGetURI(aURIString, aContentWindow);
+
+ // If the protocol handler is already registered, just return early.
+ if (this._protocolHandlerRegistered(aProtocol, uri.spec)) {
+ return;
+ }
+
+ var browserWindow = this._getBrowserWindowForContentWindow(aContentWindow);
+ if (PrivateBrowsingUtils.isWindowPrivate(browserWindow)) {
+ // Inside the private browsing mode, we don't want to alert the user to save
+ // a protocol handler. We log it to the error console so that web developers
+ // would have some way to tell what's going wrong.
+ Cc["@mozilla.org/consoleservice;1"].
+ getService(Ci.nsIConsoleService).
+ logStringMessage("Web page denied access to register a protocol handler inside private browsing mode");
+ return;
+ }
+
+ // First, check to make sure this isn't already handled internally (we don't
+ // want to let them take over, say "chrome").
+ var ios = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+ var handler = ios.getProtocolHandler(aProtocol);
+ if (!(handler instanceof Ci.nsIExternalProtocolHandler)) {
+ // This is handled internally, so we don't want them to register
+ // XXX this should be a "security exception" according to spec, but that
+ // isn't defined yet.
+ throw("Permission denied to add " + aURIString + "as a protocol handler");
+ }
+
+ // check if it is in the black list
+ var pb = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+ var allowed = pb.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "." + aProtocol,
+ pb.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "-default"));
+ if (!allowed) {
+ // XXX this should be a "security exception" according to spec
+ throw("Not allowed to register a protocol handler for " + aProtocol);
+ }
+
+ // Now Ask the user and provide the proper callback
+ var message = this._getFormattedString("addProtocolHandler",
+ [aTitle, uri.host, aProtocol]);
+
+ var notificationIcon = uri.prePath + "/favicon.ico";
+ var notificationValue = "Protocol Registration: " + aProtocol;
+ var addButton = {
+ label: this._getString("addProtocolHandlerAddButton"),
+ accessKey: this._getString("addHandlerAddButtonAccesskey"),
+ protocolInfo: { protocol: aProtocol, uri: uri.spec, name: aTitle },
+
+ callback:
+ function(aNotification, aButtonInfo) {
+ var protocol = aButtonInfo.protocolInfo.protocol;
+ var uri = aButtonInfo.protocolInfo.uri;
+ var name = aButtonInfo.protocolInfo.name;
+
+ var handler = Cc["@mozilla.org/uriloader/web-handler-app;1"].
+ createInstance(Ci.nsIWebHandlerApp);
+ handler.name = name;
+ handler.uriTemplate = uri;
+
+ var eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
+ getService(Ci.nsIExternalProtocolService);
+ var handlerInfo = eps.getProtocolHandlerInfo(protocol);
+ handlerInfo.possibleApplicationHandlers.appendElement(handler, false);
+
+ // Since the user has agreed to add a new handler, chances are good
+ // that the next time they see a handler of this type, they're going
+ // to want to use it. Reset the handlerInfo to ask before the next
+ // use.
+ handlerInfo.alwaysAskBeforeHandling = true;
+
+ var hs = Cc["@mozilla.org/uriloader/handler-service;1"].
+ getService(Ci.nsIHandlerService);
+ hs.store(handlerInfo);
+ }
+ };
+ var browserElement = this._getBrowserForContentWindow(browserWindow, aContentWindow);
+ var notificationBox = browserWindow.gBrowser.getNotificationBox(browserElement);
+ notificationBox.appendNotification(message,
+ notificationValue,
+ notificationIcon,
+ notificationBox.PRIORITY_INFO_LOW,
+ [addButton]);
+ },
+
+ /**
+ * See nsIWebContentHandlerRegistrar
+ * If a DOM window is provided, then the request came from content, so we
+ * prompt the user to confirm the registration.
+ */
+ registerContentHandler:
+ function(aContentType, aURIString, aTitle, aContentWindow) {
+ LOG("registerContentHandler(" + aContentType + "," + aURIString + "," + aTitle + ")");
+
+ // Check against the type blacklist.
+ // XXX this should be a "security exception" according to spec, but that
+ // isn't defined yet.
+ var contentType = this._resolveContentType(aContentType);
+ for (let blacklistType of TYPE_BLACKLIST) {
+ if (contentType == blacklistType) {
+ console.error("Unable to register content handler for prohibited MIME type %s.", contentType);
+ return;
+ }
+ }
+
+ if (aContentWindow) {
+ var uri = this._checkAndGetURI(aURIString, aContentWindow);
+
+ var browserWindow = this._getBrowserWindowForContentWindow(aContentWindow);
+ var browserElement = this._getBrowserForContentWindow(browserWindow, aContentWindow);
+ var notificationBox = browserWindow.gBrowser.getNotificationBox(browserElement);
+ this._appendFeedReaderNotification(uri, aTitle, notificationBox);
+ }
+ else
+ this._registerContentHandler(contentType, aURIString, aTitle);
+ },
+
+ /**
+ * Returns the browser chrome window in which the content window is in
+ */
+ _getBrowserWindowForContentWindow:
+ function(aContentWindow) {
+ return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .wrappedJSObject;
+ },
+
+ /**
+ * Returns the <xul:browser> element associated with the given content
+ * window.
+ *
+ * @param aBrowserWindow
+ * The browser window in which the content window is in.
+ * @param aContentWindow
+ * The content window. It's possible to pass a child content window
+ * (i.e. the content window of a frame/iframe).
+ */
+ _getBrowserForContentWindow:
+ function(aBrowserWindow, aContentWindow) {
+ // This depends on pseudo APIs of browser.js and tabbrowser.xml
+ aContentWindow = aContentWindow.top;
+ var browsers = aBrowserWindow.gBrowser.browsers;
+ for (var i = 0; i < browsers.length; ++i) {
+ if (browsers[i].contentWindow == aContentWindow)
+ return browsers[i];
+ }
+ },
+
+ /**
+ * Appends a notifcation for the given feed reader details.
+ *
+ * The notification could be either a pseudo-dialog which lets
+ * the user to add the feed reader:
+ * [ [icon] Add %feed-reader-name% (%feed-reader-host%) as a Feed Reader? (Add) [x] ]
+ *
+ * or a simple message for the case where the feed reader is already registered:
+ * [ [icon] %feed-reader-name% is already registered as a Feed Reader [x] ]
+ *
+ * A new notification isn't appended if the given notificationbox has a
+ * notification for the same feed reader.
+ *
+ * @param aURI
+ * The url of the feed reader as a nsIURI object
+ * @param aName
+ * The feed reader name as it was passed to registerContentHandler
+ * @param aNotificationBox
+ * The notification box to which a notification might be appended
+ * @return true if a notification has been appended, false otherwise.
+ */
+ _appendFeedReaderNotification:
+ function(aURI, aName, aNotificationBox) {
+ var uriSpec = aURI.spec;
+ var notificationValue = "feed reader notification: " + uriSpec;
+ var notificationIcon = aURI.prePath + "/favicon.ico";
+
+ // Don't append a new notification if the notificationbox
+ // has a notification for the given feed reader already
+ if (aNotificationBox.getNotificationWithValue(notificationValue))
+ return false;
+
+ var buttons, message;
+ if (this.getWebContentHandlerByURI(TYPE_MAYBE_FEED, uriSpec))
+ message = this._getFormattedString("handlerRegistered", [aName]);
+ else {
+ message = this._getFormattedString("addHandler", [aName, aURI.host]);
+ var self = this;
+ var addButton = {
+ _outer: self,
+ label: self._getString("addHandlerAddButton"),
+ accessKey: self._getString("addHandlerAddButtonAccesskey"),
+ feedReaderInfo: { uri: uriSpec, name: aName },
+
+ /* static */
+ callback:
+ function(aNotification, aButtonInfo) {
+ var uri = aButtonInfo.feedReaderInfo.uri;
+ var name = aButtonInfo.feedReaderInfo.name;
+ var outer = aButtonInfo._outer;
+
+ // The reader could have been added from another window mean while
+ if (!outer.getWebContentHandlerByURI(TYPE_MAYBE_FEED, uri))
+ outer._registerContentHandler(TYPE_MAYBE_FEED, uri, name);
+
+ // avoid reference cycles
+ aButtonInfo._outer = null;
+
+ return false;
+ }
+ };
+ buttons = [addButton];
+ }
+
+ aNotificationBox.appendNotification(message,
+ notificationValue,
+ notificationIcon,
+ aNotificationBox.PRIORITY_INFO_LOW,
+ buttons);
+ return true;
+ },
+
+ /**
+ * Save Web Content Handler metadata to persistent preferences.
+ * @param contentType
+ * The content Type being handled
+ * @param uri
+ * The uri of the web service
+ * @param title
+ * The human readable name of the web service
+ *
+ * This data is stored under:
+ *
+ * browser.contentHandlers.type0 = content/type
+ * browser.contentHandlers.uri0 = http://www.foo.com/q=%s
+ * browser.contentHandlers.title0 = Foo 2.0alphr
+ */
+ _saveContentHandlerToPrefs:
+ function(contentType, uri, title) {
+ var ps =
+ Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefService);
+ var i = 0;
+ var typeBranch = null;
+ while (true) {
+ typeBranch =
+ ps.getBranch(PREF_CONTENTHANDLERS_BRANCH + i + ".");
+ try {
+ typeBranch.getCharPref("type");
+ ++i;
+ }
+ catch (e) {
+ // No more handlers
+ break;
+ }
+ }
+ if (typeBranch) {
+ typeBranch.setCharPref("type", contentType);
+ var pls =
+ Cc["@mozilla.org/pref-localizedstring;1"].
+ createInstance(Ci.nsIPrefLocalizedString);
+ pls.data = uri;
+ typeBranch.setComplexValue("uri", Ci.nsIPrefLocalizedString, pls);
+ pls.data = title;
+ typeBranch.setComplexValue("title", Ci.nsIPrefLocalizedString, pls);
+
+ ps.savePrefFile(null);
+ }
+ },
+
+ /**
+ * Determines if there is a type with a particular uri registered for the
+ * specified content type already.
+ * @param contentType
+ * The content type that the uri handles
+ * @param uri
+ * The uri of the
+ */
+ _typeIsRegistered: function(contentType, uri) {
+ if (!(contentType in this._contentTypes))
+ return false;
+
+ var services = this._contentTypes[contentType];
+ for (var i = 0; i < services.length; ++i) {
+ // This uri has already been registered
+ if (services[i].uri == uri)
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Gets a stream converter contract id for the specified content type.
+ * @param contentType
+ * The source content type for the conversion.
+ * @returns A contract id to construct a converter to convert between the
+ * contentType and *\/*.
+ */
+ _getConverterContractID: function(contentType) {
+ const template = "@mozilla.org/streamconv;1?from=%s&to=*/*";
+ return template.replace(/%s/, contentType);
+ },
+
+ /**
+ * Register a web service handler for a content type.
+ *
+ * @param contentType
+ * the content type being handled
+ * @param uri
+ * the URI of the web service
+ * @param title
+ * the human readable name of the web service
+ */
+ _registerContentHandler:
+ function(contentType, uri, title) {
+ this._updateContentTypeHandlerMap(contentType, uri, title);
+ this._saveContentHandlerToPrefs(contentType, uri, title);
+
+ if (contentType == TYPE_MAYBE_FEED) {
+ // Make the new handler the last-selected reader in the preview page
+ // and make sure the preview page is shown the next time a feed is visited
+ var pb = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefService).getBranch(null);
+ pb.setCharPref(PREF_SELECTED_READER, "web");
+
+ var supportsString =
+ Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ supportsString.data = uri;
+ pb.setComplexValue(PREF_SELECTED_WEB, Ci.nsISupportsString,
+ supportsString);
+ pb.setCharPref(PREF_SELECTED_ACTION, "ask");
+ this._setAutoHandler(TYPE_MAYBE_FEED, null);
+ }
+ },
+
+ /**
+ * Update the content type -> handler map. This mapping is not persisted, use
+ * registerContentHandler or _saveContentHandlerToPrefs for that purpose.
+ * @param contentType
+ * The content Type being handled
+ * @param uri
+ * The uri of the web service
+ * @param title
+ * The human readable name of the web service
+ */
+ _updateContentTypeHandlerMap:
+ function(contentType, uri, title) {
+ if (!(contentType in this._contentTypes))
+ this._contentTypes[contentType] = [];
+
+ // Avoid adding duplicates
+ if (this._typeIsRegistered(contentType, uri))
+ return;
+
+ this._contentTypes[contentType].push(new ServiceInfo(contentType, uri, title));
+
+ if (!(contentType in this._blockedTypes)) {
+ var converterContractID = this._getConverterContractID(contentType);
+ var cr = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ cr.registerFactory(WCC_CLASSID, WCC_CLASSNAME, converterContractID,
+ WebContentConverterFactory);
+ }
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ getContentHandlers:
+ function(contentType, countRef) {
+ countRef.value = 0;
+ if (!(contentType in this._contentTypes))
+ return [];
+
+ var handlers = this._contentTypes[contentType];
+ countRef.value = handlers.length;
+ return handlers;
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ resetHandlersForType:
+ function(contentType) {
+ // currently unused within the tree, so only useful for extensions; previous
+ // impl. was buggy (and even infinite-looped!), so I argue that this is a
+ // definite improvement
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ /**
+ * Registers a handler from the settings on a preferences branch.
+ *
+ * @param branch
+ * an nsIPrefBranch containing "type", "uri", and "title" preferences
+ * corresponding to the content handler to be registered
+ */
+ _registerContentHandlerWithBranch: function(branch) {
+ /**
+ * Since we support up to six predefined readers, we need to handle gaps
+ * better, since the first branch with user-added values will be .6
+ *
+ * How we deal with that is to check to see if there's no prefs in the
+ * branch and stop cycling once that's true. This doesn't fix the case
+ * where a user manually removes a reader, but that's not supported yet!
+ */
+ var vals = branch.getChildList("");
+ if (vals.length == 0)
+ return;
+
+ try {
+ var type = branch.getCharPref("type");
+ var uri = branch.getComplexValue("uri", Ci.nsIPrefLocalizedString).data;
+ var title = branch.getComplexValue("title",
+ Ci.nsIPrefLocalizedString).data;
+ this._updateContentTypeHandlerMap(type, uri, title);
+ }
+ catch(ex) {
+ // do nothing, the next branch might have values
+ }
+ },
+
+ /**
+ * Load the auto handler, content handler and protocol tables from
+ * preferences.
+ */
+ _init: function() {
+ var ps =
+ Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefService);
+
+ var kids = ps.getBranch(PREF_CONTENTHANDLERS_BRANCH)
+ .getChildList("");
+
+ // first get the numbers of the providers by getting all ###.uri prefs
+ var nums = [];
+ for (var i = 0; i < kids.length; i++) {
+ var match = /^(\d+)\.uri$/.exec(kids[i]);
+ if (!match)
+ continue;
+ else
+ nums.push(match[1]);
+ }
+
+ // sort them, to get them back in order
+ nums.sort(function(a, b) {return a - b;});
+
+ // now register them
+ for (var i = 0; i < nums.length; i++) {
+ var branch = ps.getBranch(PREF_CONTENTHANDLERS_BRANCH + nums[i] + ".");
+ this._registerContentHandlerWithBranch(branch);
+ }
+
+ // We need to do this _after_ registering all of the available handlers,
+ // so that getWebContentHandlerByURI can return successfully.
+ try {
+ var autoBranch = ps.getBranch(PREF_CONTENTHANDLERS_AUTO);
+ var childPrefs = autoBranch.getChildList("");
+ for (var i = 0; i < childPrefs.length; ++i) {
+ var type = childPrefs[i];
+ var uri = autoBranch.getCharPref(type);
+ if (uri) {
+ var handler = this.getWebContentHandlerByURI(type, uri);
+ this._setAutoHandler(type, handler);
+ }
+ }
+ }
+ catch (e) {
+ // No auto branch yet, that's fine
+ //LOG("WCCR.init: There is no auto branch, benign");
+ }
+ },
+
+ /**
+ * See nsIObserver
+ */
+ observe: function(subject, topic, data) {
+ var os =
+ Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+ switch (topic) {
+ case "app-startup":
+ os.addObserver(this, "browser-ui-startup-complete", false);
+ break;
+ case "browser-ui-startup-complete":
+ os.removeObserver(this, "browser-ui-startup-complete");
+ this._init();
+ break;
+ }
+ },
+
+ /**
+ * See nsIFactory
+ */
+ createInstance: function(outer, iid) {
+ if (outer != null)
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ return this.QueryInterface(iid);
+ },
+
+ classID: WCCR_CLASSID,
+
+ /**
+ * See nsISupports
+ */
+ QueryInterface: XPCOMUtils.generateQI(
+ [Ci.nsIWebContentConverterService,
+ Ci.nsIWebContentHandlerRegistrar,
+ Ci.nsIObserver,
+ Ci.nsIFactory]),
+
+ _xpcom_categories: [{
+ category: "app-startup",
+ service: true
+ }]
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WebContentConverterRegistrar]);