Bug 1341191 - Fix messaging priv escalation of feed reader, simplify messaging from parent to child. Reduce race conditions on page. r=Gijs

This commit is contained in:
Jonathan Kingston
2017-03-19 06:29:17 +00:00
parent 7da7139ee3
commit f563862063
4 changed files with 480 additions and 339 deletions

View File

@@ -3,11 +3,136 @@
* 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/. */
XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
"resource://gre/modules/DeferredTask.jsm");
const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed";
const TYPE_MAYBE_AUDIO_FEED = "application/vnd.mozilla.maybe.audio.feed";
const TYPE_MAYBE_VIDEO_FEED = "application/vnd.mozilla.maybe.video.feed";
const PREF_SHOW_FIRST_RUN_UI = "browser.feeds.showFirstRunUI";
const PREF_SELECTED_APP = "browser.feeds.handlers.application";
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_VIDEO_SELECTED_APP = "browser.videoFeeds.handlers.application";
const PREF_VIDEO_SELECTED_WEB = "browser.videoFeeds.handlers.webservice";
const PREF_VIDEO_SELECTED_ACTION = "browser.videoFeeds.handler";
const PREF_VIDEO_SELECTED_READER = "browser.videoFeeds.handler.default";
const PREF_AUDIO_SELECTED_APP = "browser.audioFeeds.handlers.application";
const PREF_AUDIO_SELECTED_WEB = "browser.audioFeeds.handlers.webservice";
const PREF_AUDIO_SELECTED_ACTION = "browser.audioFeeds.handler";
const PREF_AUDIO_SELECTED_READER = "browser.audioFeeds.handler.default";
const PREF_UPDATE_DELAY = 2000;
const SETTABLE_PREFS = new Set([
PREF_VIDEO_SELECTED_ACTION,
PREF_AUDIO_SELECTED_ACTION,
PREF_SELECTED_ACTION,
PREF_VIDEO_SELECTED_READER,
PREF_AUDIO_SELECTED_READER,
PREF_SELECTED_READER,
PREF_VIDEO_SELECTED_WEB,
PREF_AUDIO_SELECTED_WEB,
PREF_SELECTED_WEB
]);
const EXECUTABLE_PREFS = new Set([
PREF_SELECTED_APP,
PREF_VIDEO_SELECTED_APP,
PREF_AUDIO_SELECTED_APP
]);
const VALID_ACTIONS = new Set(["ask", "reader", "bookmarks"]);
const VALID_READERS = new Set(["web", "client", "default", "bookmarks"]);
XPCOMUtils.defineLazyPreferenceGetter(this, "SHOULD_LOG",
"feeds.log", false);
function LOG(str) {
if (SHOULD_LOG)
dump("*** Feeds: " + str + "\n");
}
function getPrefActionForType(t) {
switch (t) {
case Ci.nsIFeed.TYPE_VIDEO:
return PREF_VIDEO_SELECTED_ACTION;
case Ci.nsIFeed.TYPE_AUDIO:
return PREF_AUDIO_SELECTED_ACTION;
default:
return PREF_SELECTED_ACTION;
}
}
function getPrefReaderForType(t) {
switch (t) {
case Ci.nsIFeed.TYPE_VIDEO:
return PREF_VIDEO_SELECTED_READER;
case Ci.nsIFeed.TYPE_AUDIO:
return PREF_AUDIO_SELECTED_READER;
default:
return PREF_SELECTED_READER;
}
}
function getPrefWebForType(t) {
switch (t) {
case Ci.nsIFeed.TYPE_VIDEO:
return PREF_VIDEO_SELECTED_WEB;
case Ci.nsIFeed.TYPE_AUDIO:
return PREF_AUDIO_SELECTED_WEB;
default:
return PREF_SELECTED_WEB;
}
}
function getPrefAppForType(t) {
switch (t) {
case Ci.nsIFeed.TYPE_VIDEO:
return PREF_VIDEO_SELECTED_APP;
case Ci.nsIFeed.TYPE_AUDIO:
return PREF_AUDIO_SELECTED_APP;
default:
return PREF_SELECTED_APP;
}
}
/**
* Maps a feed type to a maybe-feed mimetype.
*/
function getMimeTypeForFeedType(aFeedType) {
switch (aFeedType) {
case Ci.nsIFeed.TYPE_VIDEO:
return TYPE_MAYBE_VIDEO_FEED;
case Ci.nsIFeed.TYPE_AUDIO:
return TYPE_MAYBE_AUDIO_FEED;
default:
return TYPE_MAYBE_FEED;
}
}
/**
* The Feed Handler object manages discovery of RSS/ATOM feeds in web pages
* and shows UI when they are discovered.
*/
var FeedHandler = {
_prefChangeCallback: null,
/** Called when the user clicks on the Subscribe to This Page... menu item,
* or when the user clicks the feed button when the page contains multiple
* feeds.
@@ -195,7 +320,8 @@ var FeedHandler = {
return file.leafName;
},
chooseClientApp(aTitle, aPrefName, aBrowser) {
_chooseClientApp(aTitle, aTypeName, aBrowser) {
const prefName = getPrefAppForType(aTypeName);
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
fp.init(window, aTitle, Ci.nsIFilePicker.modeOpen);
@@ -222,7 +348,7 @@ var FeedHandler = {
}
if (fp.file.leafName != appName) {
Services.prefs.setComplexValue(aPrefName, Ci.nsILocalFile, selectedApp);
Services.prefs.setComplexValue(prefName, Ci.nsILocalFile, selectedApp);
aBrowser.messageManager.sendAsyncMessage("FeedWriter:SetApplicationLauncherMenuItem",
{ name: this._getFileDisplayName(selectedApp),
type: "SelectedAppMenuItem" });
@@ -277,70 +403,243 @@ var FeedHandler = {
}
},
// nsISupports
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsISupportsWeakReference]),
init() {
window.messageManager.addMessageListener("FeedWriter:ChooseClientApp", this);
window.messageManager.addMessageListener("FeedWriter:RequestClientAppName", this);
window.messageManager.addMessageListener("FeedWriter:SetFeedCharPref", this);
window.messageManager.addMessageListener("FeedWriter:SetFeedComplexString", this);
window.messageManager.addMessageListener("FeedWriter:GetSubscriptionUI", this);
window.messageManager.addMessageListener("FeedWriter:SetFeedPrefsAndSubscribe", this);
window.messageManager.addMessageListener("FeedWriter:ShownFirstRun", this);
Services.ppmm.addMessageListener("FeedConverter:ExecuteClientApp", this);
const prefs = Services.prefs;
prefs.addObserver(PREF_SELECTED_ACTION, this, true);
prefs.addObserver(PREF_SELECTED_READER, this, true);
prefs.addObserver(PREF_SELECTED_WEB, this, true);
prefs.addObserver(PREF_VIDEO_SELECTED_ACTION, this, true);
prefs.addObserver(PREF_VIDEO_SELECTED_READER, this, true);
prefs.addObserver(PREF_VIDEO_SELECTED_WEB, this, true);
prefs.addObserver(PREF_AUDIO_SELECTED_ACTION, this, true);
prefs.addObserver(PREF_AUDIO_SELECTED_READER, this, true);
prefs.addObserver(PREF_AUDIO_SELECTED_WEB, this, true);
},
uninit() {
Services.ppmm.removeMessageListener("FeedConverter:ExecuteClientApp", this);
this._prefChangeCallback = null;
},
// nsIObserver
observe(subject, topic, data) {
if (topic == "nsPref:changed") {
LOG(`Pref changed ${data}`)
if (this._prefChangeCallback) {
this._prefChangeCallback.disarm();
}
// Multiple prefs are set at the same time, debounce to reduce noise
// This can happen in one feed and we want to message all feed pages
this._prefChangeCallback = new DeferredTask(() => {
this._prefChanged(data);
}, PREF_UPDATE_DELAY);
this._prefChangeCallback.arm();
}
},
_prefChanged(prefName) {
// Don't observe for PREF_*SELECTED_APP as user likely just picked one
// That is also handled by SetApplicationLauncherMenuItem call
// Rather than the others which happen on subscription
switch (prefName) {
case PREF_SELECTED_READER:
case PREF_SELECTED_WEB:
case PREF_VIDEO_SELECTED_READER:
case PREF_VIDEO_SELECTED_WEB:
case PREF_AUDIO_SELECTED_READER:
case PREF_AUDIO_SELECTED_WEB:
case PREF_SELECTED_ACTION:
case PREF_VIDEO_SELECTED_ACTION:
case PREF_AUDIO_SELECTED_ACTION:
const response = {
default: this._getReaderForType(Ci.nsIFeed.TYPE_FEED),
[Ci.nsIFeed.TYPE_AUDIO]: this._getReaderForType(Ci.nsIFeed.TYPE_AUDIO),
[Ci.nsIFeed.TYPE_VIDEO]: this._getReaderForType(Ci.nsIFeed.TYPE_VIDEO)
};
Services.mm.broadcastAsyncMessage("FeedWriter:PreferenceUpdated",
response);
break;
}
},
_initSubscriptionUIResponse(feedType) {
const wccr = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"].
getService(Ci.nsIWebContentConverterService);
const handlersRaw = wccr.getContentHandlers(getMimeTypeForFeedType(feedType));
const handlers = [];
for (let handler of handlersRaw) {
LOG(`Handler found: ${handler}`);
handlers.push({
name: handler.name,
uri: handler.uri
});
}
let showFirstRunUI = true;
// eslint-disable-next-line mozilla/use-default-preference-values
try {
showFirstRunUI = Services.prefs.getBoolPref(PREF_SHOW_FIRST_RUN_UI);
} catch (ex) { }
const response = { handlers, showFirstRunUI };
let selectedClientApp;
const feedTypePref = getPrefAppForType(feedType);
try {
selectedClientApp = Services.prefs.getComplexValue(feedTypePref, Ci.nsILocalFile);
} catch (ex) {
// Just do nothing, then we won't bother populating
}
let defaultClientApp = null;
try {
// This can sometimes not exist
defaultClientApp = Cc["@mozilla.org/browser/shell-service;1"]
.getService(Ci.nsIShellService)
.defaultFeedReader;
} catch (ex) {
// Just do nothing, then we don't bother populating
}
if (selectedClientApp && selectedClientApp.exists()) {
if (defaultClientApp && selectedClientApp.path != defaultClientApp.path) {
// Only set the default menu item if it differs from the selected one
response.defaultMenuItem = this._getFileDisplayName(defaultClientApp);
}
response.selectedMenuItem = this._getFileDisplayName(selectedClientApp);
}
response.reader = this._getReaderForType(feedType);
return response;
},
_setPref(aPrefName, aPrefValue, aIsComplex = false) {
LOG(`FeedWriter._setPref ${aPrefName}`);
// Ensure we have a pref that is settable
if (aPrefName && SETTABLE_PREFS.has(aPrefName)) {
if (aIsComplex) {
const supportsString = Cc["@mozilla.org/supports-string;1"].
createInstance(Ci.nsISupportsString);
supportsString.data = aPrefValue;
Services.prefs.setComplexValue(aPrefName, Ci.nsISupportsString, supportsString);
} else {
Services.prefs.setCharPref(aPrefName, aPrefValue);
}
} else {
LOG(`FeedWriter._setPref ${aPrefName} not allowed`);
}
},
_getReaderForType(feedType) {
let prefs = Services.prefs;
let handler = "bookmarks";
let url;
// eslint-disable-next-line mozilla/use-default-preference-values
try {
handler = prefs.getCharPref(getPrefReaderForType(feedType));
} catch (ex) { }
if (handler === "web") {
try {
url = prefs.getComplexValue(getPrefWebForType(feedType), Ci.nsISupportsString).data;
} catch (ex) {
LOG("FeedWriter._setSelectedHandler: invalid or no handler in prefs");
url = null;
}
}
const alwaysUse = this._getAlwaysUseState(feedType);
const action = prefs.getCharPref(getPrefActionForType(feedType));
return { handler, url, alwaysUse, action };
},
_getAlwaysUseState(feedType) {
try {
return Services.prefs.getCharPref(getPrefActionForType(feedType)) != "ask";
} catch (ex) { }
return false;
},
receiveMessage(msg) {
let handler;
switch (msg.name) {
case "FeedWriter:ChooseClientApp":
this.chooseClientApp(msg.data.title, msg.data.prefName, msg.target);
case "FeedWriter:GetSubscriptionUI":
const response = this._initSubscriptionUIResponse(msg.data.feedType);
msg.target.messageManager
.sendAsyncMessage("FeedWriter:GetSubscriptionUIResponse",
response);
break;
case "FeedWriter:RequestClientAppName":
let selectedClientApp;
try {
selectedClientApp = Services.prefs.getComplexValue(msg.data.feedTypePref, Ci.nsILocalFile);
} catch (ex) {
// Just do nothing, then we won't bother populating
}
let defaultClientApp = null;
try {
// This can sometimes not exist
defaultClientApp = Cc["@mozilla.org/browser/shell-service;1"]
.getService(Ci.nsIShellService)
.defaultFeedReader;
} catch (ex) {
// Just do nothing, then we don't bother populating
}
if (selectedClientApp && selectedClientApp.exists()) {
if (defaultClientApp && selectedClientApp.path != defaultClientApp.path) {
// Only set the default menu item if it differs from the selected one
msg.target.messageManager
.sendAsyncMessage("FeedWriter:SetApplicationLauncherMenuItem",
{ name: this._getFileDisplayName(defaultClientApp),
type: "DefaultAppMenuItem" });
}
msg.target.messageManager
.sendAsyncMessage("FeedWriter:SetApplicationLauncherMenuItem",
{ name: this._getFileDisplayName(selectedClientApp),
type: "SelectedAppMenuItem" });
}
case "FeedWriter:ChooseClientApp":
this._chooseClientApp(msg.data.title, msg.data.feedType, msg.target);
break;
case "FeedWriter:ShownFirstRun":
Services.prefs.setBoolPref("browser.feeds.showFirstRunUI", false);
Services.prefs.setBoolPref(PREF_SHOW_FIRST_RUN_UI, false);
break;
case "FeedWriter:SetFeedCharPref":
Services.prefs.setCharPref(msg.data.pref, msg.data.value);
break;
case "FeedWriter:SetFeedComplexString": {
Services.prefs.setStringPref(msg.data.pref, msg.data.value);
break;
}
case "FeedWriter:SetFeedPrefsAndSubscribe":
const settings = msg.data;
if (!settings.action || !VALID_ACTIONS.has(settings.action)) {
LOG(`Invalid action ${settings.action}`);
return;
}
if (!settings.reader || !VALID_READERS.has(settings.reader)) {
LOG(`Invalid reader ${settings.reader}`);
return;
}
const actionPref = getPrefActionForType(settings.feedType);
this._setPref(actionPref, settings.action);
const readerPref = getPrefReaderForType(settings.feedType);
this._setPref(readerPref, settings.reader);
handler = null;
switch (settings.reader) {
case "web":
// This is a web set URI by content using window.registerContentHandler()
// Lets make sure we know about it before setting it
const webPref = getPrefWebForType(settings.feedType);
let wccr = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"].
getService(Ci.nsIWebContentConverterService);
// If the user provided an invalid web URL this function won't give us a reference
handler = wccr.getWebContentHandlerByURI(getMimeTypeForFeedType(settings.feedType), settings.uri);
if (handler) {
this._setPref(webPref, settings.uri, true);
if (settings.useAsDefault) {
wccr.setAutoHandler(getMimeTypeForFeedType(settings.feedType), handler);
}
msg.target.messageManager
.sendAsyncMessage("FeedWriter:SetFeedPrefsAndSubscribeResponse",
{ redirect: handler.getHandlerURI(settings.feedLocation) });
} else {
LOG(`No handler found for web ${settings.feedType} ${settings.uri}`);
}
break;
default:
const feedService = Cc["@mozilla.org/browser/feeds/result-service;1"].
getService(Ci.nsIFeedResultService);
feedService.addToClientReader(settings.feedLocation,
settings.feedTitle,
settings.feedSubtitle,
settings.feedType,
settings.reader);
}
break;
case "FeedConverter:ExecuteClientApp":
this.executeClientApp(msg.data.spec, msg.data.title,
msg.data.subtitle, msg.data.feedHandler);
// Always check feedHandler is from a set array of executable prefs
if (EXECUTABLE_PREFS.has(msg.data.feedHandler)) {
this.executeClientApp(msg.data.spec, msg.data.title,
msg.data.subtitle, msg.data.feedHandler);
} else {
LOG(`FeedConverter:ExecuteClientApp - Will not exec ${msg.data.feedHandler}`);
}
break;
}
},