summaryrefslogtreecommitdiffstats
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.js1071
1 files changed, 1071 insertions, 0 deletions
diff --git a/browser/components/feeds/WebContentConverter.js b/browser/components/feeds/WebContentConverter.js
new file mode 100644
index 000000000..2cb5cd145
--- /dev/null
+++ b/browser/components/feeds/WebContentConverter.js
@@ -0,0 +1,1071 @@
+/* -*- 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/Services.jsm");
+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 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() { },
+ asyncConvertData() { },
+ onDataAvailable() { },
+ onStopRequest() { },
+
+ onStartRequest(request, context) {
+ let wccr =
+ Cc[WCCR_CONTRACTID].
+ getService(Ci.nsIWebContentConverterService);
+ wccr.loadPreferredHandler(request);
+ },
+
+ QueryInterface(iid) {
+ if (iid.equals(Ci.nsIStreamConverter) ||
+ iid.equals(Ci.nsIStreamListener) ||
+ iid.equals(Ci.nsISupports))
+ return this;
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+};
+
+let WebContentConverterFactory = {
+ createInstance(outer, iid) {
+ if (outer != null)
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ return new WebContentConverter().QueryInterface(iid);
+ },
+
+ QueryInterface(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(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(uri) {
+ return this._uri.replace(/%s/gi, encodeURIComponent(uri));
+ },
+
+ QueryInterface(iid) {
+ if (iid.equals(Ci.nsIWebContentHandlerInfo) ||
+ iid.equals(Ci.nsISupports))
+ return this;
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+};
+
+const Utils = {
+ makeURI(aURL, aOriginCharset, aBaseURI) {
+ return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
+ },
+
+ checkAndGetURI(aURIString, aContentWindow) {
+ let uri;
+ try {
+ let baseURI = aContentWindow.document.baseURIObject;
+ uri = this.makeURI(aURIString, null, baseURI);
+ } catch (ex) {
+ throw NS_ERROR_DOM_SYNTAX_ERR;
+ }
+
+ // For security reasons we reject non-http(s) urls (see bug 354316),
+ // we may need to revise this once we support more content types
+ if (uri.scheme != "http" && uri.scheme != "https") {
+ throw this.getSecurityError(
+ "Permission denied to add " + uri.spec + " as a content or protocol handler",
+ aContentWindow);
+ }
+
+ // We also reject handlers registered from a different host (see bug 402287)
+ // The pref allows us to test the feature
+ let pb = Services.prefs;
+ if (!pb.getBoolPref(PREF_ALLOW_DIFFERENT_HOST) &&
+ (!["http:", "https:"].includes(aContentWindow.location.protocol) ||
+ aContentWindow.location.hostname != uri.host)) {
+ throw this.getSecurityError(
+ "Permission denied to add " + uri.spec + " as a content or protocol handler",
+ aContentWindow);
+ }
+
+ // 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;
+ },
+
+ // NB: Throws if aProtocol is not allowed.
+ checkProtocolHandlerAllowed(aProtocol, aURIString, aWindowOrNull) {
+ // First, check to make sure this isn't already handled internally (we don't
+ // want to let them take over, say "chrome").
+ let handler = Services.io.getProtocolHandler(aProtocol);
+ if (!(handler instanceof Ci.nsIExternalProtocolHandler)) {
+ // This is handled internally, so we don't want them to register
+ throw this.getSecurityError(
+ `Permission denied to add ${aURIString} as a protocol handler`,
+ aWindowOrNull);
+ }
+
+ // check if it is in the black list
+ let pb = Services.prefs;
+ let allowed;
+ try {
+ allowed = pb.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "." + aProtocol);
+ }
+ catch (e) {
+ allowed = pb.getBoolPref(PREF_HANDLER_EXTERNAL_PREFIX + "-default");
+ }
+ if (!allowed) {
+ throw this.getSecurityError(
+ `Not allowed to register a protocol handler for ${aProtocol}`,
+ aWindowOrNull);
+ }
+ },
+
+ // Return a SecurityError exception from the given Window if one is given. If
+ // none is given, just return the given error string, for lack of anything
+ // better.
+ getSecurityError(errorString, aWindowOrNull) {
+ if (!aWindowOrNull) {
+ return errorString;
+ }
+
+ return new aWindowOrNull.DOMException(errorString, "SecurityError");
+ },
+
+ /**
+ * Mappings from known feed types to our internal content type.
+ */
+ _mappings: {
+ "application/rss+xml": TYPE_MAYBE_FEED,
+ "application/atom+xml": TYPE_MAYBE_FEED,
+ },
+
+ resolveContentType(aContentType) {
+ if (aContentType in this._mappings)
+ return this._mappings[aContentType];
+ return aContentType;
+ }
+};
+
+function WebContentConverterRegistrar() {
+ this._contentTypes = {};
+ this._autoHandleContentTypes = {};
+}
+
+WebContentConverterRegistrar.prototype = {
+ get stringBundle() {
+ let sb = Services.strings.createBundle(STRING_BUNDLE_URI);
+ delete WebContentConverterRegistrar.prototype.stringBundle;
+ return WebContentConverterRegistrar.prototype.stringBundle = sb;
+ },
+
+ _getFormattedString(key, params) {
+ return this.stringBundle.formatStringFromName(key, params, params.length);
+ },
+
+ _getString(key) {
+ return this.stringBundle.GetStringFromName(key);
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ getAutoHandler(contentType) {
+ contentType = Utils.resolveContentType(contentType);
+ if (contentType in this._autoHandleContentTypes)
+ return this._autoHandleContentTypes[contentType];
+ return null;
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ setAutoHandler(contentType, handler) {
+ if (handler && !this._typeIsRegistered(contentType, handler.uri))
+ throw Cr.NS_ERROR_NOT_AVAILABLE;
+
+ contentType = Utils.resolveContentType(contentType);
+ this._setAutoHandler(contentType, handler);
+
+ let ps = Services.prefs;
+ let 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(contentType, handler) {
+ if (handler)
+ this._autoHandleContentTypes[contentType] = handler;
+ else if (contentType in this._autoHandleContentTypes)
+ delete this._autoHandleContentTypes[contentType];
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ getWebContentHandlerByURI(contentType, uri) {
+ return this.getContentHandlers(contentType)
+ .find(e => e.uri == uri) || null;
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ loadPreferredHandler(request) {
+ let channel = request.QueryInterface(Ci.nsIChannel);
+ let contentType = Utils.resolveContentType(channel.contentType);
+ let handler = this.getAutoHandler(contentType);
+ if (handler) {
+ request.cancel(Cr.NS_ERROR_FAILURE);
+
+ let webNavigation =
+ channel.notificationCallbacks.getInterface(Ci.nsIWebNavigation);
+ webNavigation.loadURI(handler.getHandlerURI(channel.URI.spec),
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
+ null, null, null);
+ }
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ removeProtocolHandler(aProtocol, aURITemplate) {
+ let eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
+ getService(Ci.nsIExternalProtocolService);
+ let handlerInfo = eps.getProtocolHandlerInfo(aProtocol);
+ let 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);
+ let 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(contentType, uri) {
+ function notURI(serviceInfo) {
+ return serviceInfo.uri != uri;
+ }
+
+ if (contentType in this._contentTypes) {
+ this._contentTypes[contentType] =
+ this._contentTypes[contentType].filter(notURI);
+ }
+ },
+
+ /**
+ * 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 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(aProtocol, aURITemplate) {
+ let eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
+ getService(Ci.nsIExternalProtocolService);
+ let handlerInfo = eps.getProtocolHandlerInfo(aProtocol);
+ let 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(aProtocol, aURIString, aTitle, aBrowserOrWindow) {
+ LOG("registerProtocolHandler(" + aProtocol + "," + aURIString + "," + aTitle + ")");
+ let haveWindow = (aBrowserOrWindow instanceof Ci.nsIDOMWindow);
+ let uri;
+ if (haveWindow) {
+ uri = Utils.checkAndGetURI(aURIString, aBrowserOrWindow);
+ } else {
+ // aURIString must not be a relative URI.
+ uri = Utils.makeURI(aURIString, null);
+ }
+
+ // If the protocol handler is already registered, just return early.
+ if (this._protocolHandlerRegistered(aProtocol, uri.spec)) {
+ return;
+ }
+
+ let browser;
+ if (haveWindow) {
+ let browserWindow =
+ this._getBrowserWindowForContentWindow(aBrowserOrWindow);
+ browser = this._getBrowserForContentWindow(browserWindow,
+ aBrowserOrWindow);
+ } else {
+ browser = aBrowserOrWindow;
+ }
+ if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
+ // 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.
+ Services.console.
+ logStringMessage("Web page denied access to register a protocol handler inside private browsing mode");
+ return;
+ }
+
+ Utils.checkProtocolHandlerAllowed(aProtocol, aURIString,
+ haveWindow ? aBrowserOrWindow : null);
+
+ // Now Ask the user and provide the proper callback
+ let message = this._getFormattedString("addProtocolHandler",
+ [aTitle, uri.host, aProtocol]);
+
+ let notificationIcon = uri.prePath + "/favicon.ico";
+ let notificationValue = "Protocol Registration: " + aProtocol;
+ let addButton = {
+ label: this._getString("addProtocolHandlerAddButton"),
+ accessKey: this._getString("addProtocolHandlerAddButtonAccesskey"),
+ protocolInfo: { protocol: aProtocol, uri: uri.spec, name: aTitle },
+
+ callback(aNotification, aButtonInfo) {
+ let protocol = aButtonInfo.protocolInfo.protocol;
+ let uri = aButtonInfo.protocolInfo.uri;
+ let name = aButtonInfo.protocolInfo.name;
+
+ let handler = Cc["@mozilla.org/uriloader/web-handler-app;1"].
+ createInstance(Ci.nsIWebHandlerApp);
+ handler.name = name;
+ handler.uriTemplate = uri;
+
+ let eps = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
+ getService(Ci.nsIExternalProtocolService);
+ let 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;
+
+ let hs = Cc["@mozilla.org/uriloader/handler-service;1"].
+ getService(Ci.nsIHandlerService);
+ hs.store(handlerInfo);
+ }
+ };
+ let notificationBox = browser.getTabBrowser().getNotificationBox(browser);
+ 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(aContentType, aURIString, aTitle, aWindowOrBrowser) {
+ LOG("registerContentHandler(" + aContentType + "," + aURIString + "," + aTitle + ")");
+
+ // Make sure to do our URL checks up front, before our content type check,
+ // just like the WebContentConverterRegistrarContent does.
+ let haveWindow = aWindowOrBrowser &&
+ (aWindowOrBrowser instanceof Ci.nsIDOMWindow);
+ let uri;
+ if (haveWindow) {
+ uri = Utils.checkAndGetURI(aURIString, aWindowOrBrowser);
+ } else if (aWindowOrBrowser) {
+ // uri was vetted in the content process.
+ uri = Utils.makeURI(aURIString, null);
+ }
+
+ // We only support feed types at present.
+ let contentType = Utils.resolveContentType(aContentType);
+ // XXX We should be throwing a Utils.getSecurityError() here in at least
+ // some cases. See bug 1266492.
+ if (contentType != TYPE_MAYBE_FEED) {
+ return;
+ }
+
+ if (aWindowOrBrowser) {
+ let notificationBox;
+ if (haveWindow) {
+ let browserWindow = this._getBrowserWindowForContentWindow(aWindowOrBrowser);
+ let browserElement = this._getBrowserForContentWindow(browserWindow, aWindowOrBrowser);
+ notificationBox = browserElement.getTabBrowser().getNotificationBox(browserElement);
+ } else {
+ notificationBox = aWindowOrBrowser.getTabBrowser()
+ .getNotificationBox(aWindowOrBrowser);
+ }
+
+ this._appendFeedReaderNotification(uri, aTitle, notificationBox);
+ }
+ else {
+ this._registerContentHandler(contentType, aURIString, aTitle);
+ }
+ },
+
+ /**
+ * Returns the browser chrome window in which the content window is in
+ */
+ _getBrowserWindowForContentWindow(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(aBrowserWindow, aContentWindow) {
+ // This depends on pseudo APIs of browser.js and tabbrowser.xml
+ aContentWindow = aContentWindow.top;
+ return aBrowserWindow.gBrowser.browsers.find((browser) =>
+ browser.contentWindow == aContentWindow);
+ },
+
+ /**
+ * 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(aURI, aName, aNotificationBox) {
+ let uriSpec = aURI.spec;
+ let notificationValue = "feed reader notification: " + uriSpec;
+ let 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;
+
+ let buttons;
+ let message;
+ if (this.getWebContentHandlerByURI(TYPE_MAYBE_FEED, uriSpec))
+ message = this._getFormattedString("handlerRegistered", [aName]);
+ else {
+ message = this._getFormattedString("addHandler", [aName, aURI.host]);
+ let self = this;
+ let addButton = {
+ _outer: self,
+ label: self._getString("addHandlerAddButton"),
+ accessKey: self._getString("addHandlerAddButtonAccesskey"),
+ feedReaderInfo: { uri: uriSpec, name: aName },
+
+ /* static */
+ callback(aNotification, aButtonInfo) {
+ let uri = aButtonInfo.feedReaderInfo.uri;
+ let name = aButtonInfo.feedReaderInfo.name;
+ let 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(contentType, uri, title) {
+ let ps = Services.prefs;
+ let i = 0;
+ let 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);
+ let 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 content type
+ */
+ _typeIsRegistered(contentType, uri) {
+ if (!(contentType in this._contentTypes))
+ return false;
+
+ return this._contentTypes[contentType]
+ .some(t => t.uri == uri);
+ },
+
+ /**
+ * 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(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(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
+ let pb = Services.prefs.getBranch(null);
+ pb.setCharPref(PREF_SELECTED_READER, "web");
+
+ let 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(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)) {
+ let converterContractID = this._getConverterContractID(contentType);
+ let cr = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ cr.registerFactory(WCC_CLASSID, WCC_CLASSNAME, converterContractID,
+ WebContentConverterFactory);
+ }
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ getContentHandlers(contentType, countRef) {
+ if (countRef) {
+ countRef.value = 0;
+ }
+ if (!(contentType in this._contentTypes))
+ return [];
+
+ let handlers = this._contentTypes[contentType];
+ if (countRef) {
+ countRef.value = handlers.length;
+ }
+ return handlers;
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ resetHandlersForType(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.
+ *
+ * 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!
+ *
+ * @param branch
+ * an nsIPrefBranch containing "type", "uri", and "title" preferences
+ * corresponding to the content handler to be registered
+ */
+ _registerContentHandlerHavingBranch(branch) {
+ let vals = branch.getChildList("");
+ if (vals.length == 0)
+ return;
+
+ let type = branch.getCharPref("type");
+ let uri = branch.getComplexValue("uri", Ci.nsIPrefLocalizedString).data;
+ let title = branch.getComplexValue("title",
+ Ci.nsIPrefLocalizedString).data;
+ this._updateContentTypeHandlerMap(type, uri, title);
+ },
+
+ /**
+ * Load the auto handler, content handler and protocol tables from
+ * preferences.
+ */
+ _init() {
+ let ps = Services.prefs;
+
+ let children = ps.getBranch(PREF_CONTENTHANDLERS_BRANCH)
+ .getChildList("");
+
+ // first get the numbers of the providers by getting all ###.uri prefs
+ let nums = children.map((child) => {
+ let match = /^(\d+)\.uri$/.exec(child);
+ return match ? match[1] : "";
+ }).filter(child => !!child)
+ .sort();
+
+
+ // now register them
+ for (let num of nums) {
+ let branch = ps.getBranch(PREF_CONTENTHANDLERS_BRANCH + num + ".");
+ try {
+ this._registerContentHandlerHavingBranch(branch);
+ } catch (ex) {
+ // do nothing, the next branch might have values
+ }
+ }
+
+ // We need to do this _after_ registering all of the available handlers,
+ // so that getWebContentHandlerByURI can return successfully.
+ let autoBranch;
+ try {
+ autoBranch = ps.getBranch(PREF_CONTENTHANDLERS_AUTO);
+ } catch (e) {
+ // No auto branch yet, that's fine
+ // LOG("WCCR.init: There is no auto branch, benign");
+ }
+
+ if (autoBranch) {
+ for (let type of autoBranch.getChildList("")) {
+ let uri = autoBranch.getCharPref(type);
+ if (uri) {
+ let handler = this.getWebContentHandlerByURI(type, uri);
+ if (handler) {
+ this._setAutoHandler(type, handler);
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * See nsIObserver
+ */
+ observe(subject, topic, data) {
+ let os = Services.obs;
+ 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(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
+ }]
+};
+
+function WebContentConverterRegistrarContent() {
+ this._contentTypes = {};
+}
+
+WebContentConverterRegistrarContent.prototype = {
+
+ /**
+ * Load the auto handler, content handler and protocol tables from
+ * preferences.
+ */
+ _init() {
+ let ps = Services.prefs;
+
+ let children = ps.getBranch(PREF_CONTENTHANDLERS_BRANCH)
+ .getChildList("");
+
+ // first get the numbers of the providers by getting all ###.uri prefs
+ let nums = children.map((child) => {
+ let match = /^(\d+)\.uri$/.exec(child);
+ return match ? match[1] : "";
+ }).filter(child => !!child)
+ .sort();
+
+ // now register them
+ for (num of nums) {
+ let branch = ps.getBranch(PREF_CONTENTHANDLERS_BRANCH + num + ".");
+ try {
+ this._registerContentHandlerHavingBranch(branch);
+ } catch (ex) {
+ // do nothing, the next branch might have values
+ }
+ }
+ },
+
+ _typeIsRegistered(contentType, uri) {
+ return this._contentTypes[contentType]
+ .some(e => e.uri == uri);
+ },
+
+ /**
+ * 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!
+ *
+ * @param branch
+ * The pref branch to register the content handler under
+ *
+ */
+ _registerContentHandlerHavingBranch(branch) {
+ let vals = branch.getChildList("");
+ if (vals.length == 0)
+ return;
+
+ let type = branch.getCharPref("type");
+ let uri = branch.getComplexValue("uri", Ci.nsIPrefLocalizedString).data;
+ let title = branch.getComplexValue("title",
+ Ci.nsIPrefLocalizedString).data;
+ this._updateContentTypeHandlerMap(type, uri, title);
+ },
+
+ _updateContentTypeHandlerMap(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)) {
+ let converterContractID = this._getConverterContractID(contentType);
+ let cr = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ cr.registerFactory(WCC_CLASSID, WCC_CLASSNAME, converterContractID,
+ WebContentConverterFactory);
+ }
+ },
+
+ /**
+ * See nsIWebContentConverterService
+ */
+ getContentHandlers(contentType, countRef) {
+ this._init();
+ if (countRef) {
+ countRef.value = 0;
+ }
+
+ if (!(contentType in this._contentTypes))
+ return [];
+
+ let handlers = this._contentTypes[contentType];
+ if (countRef) {
+ countRef.value = handlers.length;
+ }
+ return handlers;
+ },
+
+ setAutoHandler(contentType, handler) {
+ Services.cpmm.sendAsyncMessage("WCCR:setAutoHandler",
+ { contentType, handler });
+ },
+
+ getWebContentHandlerByURI(contentType, uri) {
+ return this.getContentHandlers(contentType)
+ .find(e => e.uri == uri) || null;
+ },
+
+ /**
+ * See nsIWebContentHandlerRegistrar
+ */
+ registerContentHandler(aContentType, aURIString, aTitle, aBrowserOrWindow) {
+ // aBrowserOrWindow must be a window.
+ let messageManager = aBrowserOrWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsITabChild)
+ .messageManager;
+
+ let uri = Utils.checkAndGetURI(aURIString, aBrowserOrWindow);
+ // XXX We should be throwing a Utils.getSecurityError() here in at least
+ // some cases. See bug 1266492.
+ if (Utils.resolveContentType(aContentType) != TYPE_MAYBE_FEED) {
+ return;
+ }
+
+ messageManager.sendAsyncMessage("WCCR:registerContentHandler",
+ { contentType: aContentType,
+ uri: uri.spec,
+ title: aTitle });
+ },
+
+ registerProtocolHandler(aProtocol, aURIString, aTitle, aBrowserOrWindow) {
+ // aBrowserOrWindow must be a window.
+ let messageManager = aBrowserOrWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsITabChild)
+ .messageManager;
+
+ let uri = Utils.checkAndGetURI(aURIString, aBrowserOrWindow);
+ Utils.checkProtocolHandlerAllowed(aProtocol, aURIString, aBrowserOrWindow);
+
+ messageManager.sendAsyncMessage("WCCR:registerProtocolHandler",
+ { protocol: aProtocol,
+ uri: uri.spec,
+ title: aTitle });
+ },
+
+ /**
+ * See nsIFactory
+ */
+ createInstance(outer, iid) {
+ if (outer != null)
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ return this.QueryInterface(iid);
+ },
+
+ classID: WCCR_CLASSID,
+
+ /**
+ * See nsISupports
+ */
+ QueryInterface: XPCOMUtils.generateQI(
+ [Ci.nsIWebContentHandlerRegistrar,
+ Ci.nsIWebContentConverterService,
+ Ci.nsIFactory])
+};
+
+this.NSGetFactory =
+ (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) ?
+ XPCOMUtils.generateNSGetFactory([WebContentConverterRegistrarContent]) :
+ XPCOMUtils.generateNSGetFactory([WebContentConverterRegistrar]);