Bug 839500 - Close system persona iframe when not in use. r=fabrice, r=benadida

This commit is contained in:
Jed Parsons 2013-03-14 15:19:34 -04:00
Родитель bbff6230ec
Коммит 9149c0c3de
5 изменённых файлов: 201 добавлений и 81 удалений

Просмотреть файл

@ -89,6 +89,7 @@ function doInternalWatch() {
},
JSON.stringify(options),
function(...things) {
// internal watch log callback
log("(watch) internal: ", things);
}
);
@ -136,7 +137,7 @@ addEventListener("DOMContentLoaded", function(e) {
// listen for request
addMessageListener(kIdentityDelegateRequest, function(aMessage) {
log("injected identity.js received", kIdentityDelegateRequest, "\n\n\n");
log("injected identity.js received", kIdentityDelegateRequest);
options = aMessage.json;
showUI = true;
func = doInternalRequest;
@ -145,7 +146,7 @@ addMessageListener(kIdentityDelegateRequest, function(aMessage) {
// listen for watch
addMessageListener(kIdentityDelegateWatch, function(aMessage) {
log("injected identity.js received", kIdentityDelegateWatch, "\n\n\n");
log("injected identity.js received", kIdentityDelegateWatch);
options = aMessage.json;
showUI = false;
func = doInternalWatch;
@ -154,7 +155,7 @@ addMessageListener(kIdentityDelegateWatch, function(aMessage) {
// listen for logout
addMessageListener(kIdentityDelegateLogout, function(aMessage) {
log("injected identity.js received", kIdentityDelegateLogout, "\n\n\n");
log("injected identity.js received", kIdentityDelegateLogout);
options = aMessage.json;
showUI = false;
func = doInternalLogout;

Просмотреть файл

@ -25,7 +25,7 @@
*
* In order for navigator.id.request() to maintain state in a single
* cookie jar, we cause all Persona interactions to take place in a
* gaia context that is launched by the system application, with the
* content context that is launched by the system application, with the
* result that Persona has a single cookie jar that all Relying
* Parties can use. Since of course those Relying Parties cannot
* reach into the system cookie jar, the Controller in this module
@ -43,10 +43,10 @@
* requesting Persona functions (doWatch, doReady, doLogout).
*
* The Identity service sends these observer messages to the
* Controller in this module, which in turn triggers gaia to open a
* Controller in this module, which in turn triggers content to open a
* window to host the Persona js. If user interaction is required,
* gaia will open the trusty UI. If user interaction is not required,
* and we only need to get to Persona functions, gaia will open a
* content will open the trusty UI. If user interaction is not required,
* and we only need to get to Persona functions, content will open a
* hidden iframe. In either case, a window is opened into which the
* controller causes the script identity.js to be injected. This
* script provides the glue between the in-page javascript and the
@ -89,11 +89,11 @@ XPCOMUtils.defineLazyModuleGetter(this, "Logger",
const kIdentityShimFile = "chrome://browser/content/identity.js";
// Type of MozChromeEvents to handle id dialogs.
const kOpenIdentityDialog = "open-id-dialog";
const kCloseIdentityDialog = "close-id-dialog";
const kOpenIdentityDialog = "id-dialog-open";
const kDoneIdentityDialog = "id-dialog-done";
const kCloseIdentityDialog = "id-dialog-close-iframe";
// Observer messages to communicate to shim
const kReceivedIdentityAssertion = "received-id-assertion";
const kIdentityDelegateWatch = "identity-delegate-watch";
const kIdentityDelegateRequest = "identity-delegate-request";
const kIdentityDelegateLogout = "identity-delegate-logout";
@ -107,12 +107,12 @@ function log(...aMessageArgs) {
}
/*
* GaiaInterface encapsulates the our gaia functions. There are only two:
* ContentInterface encapsulates the our content functions. There are only two:
*
* getContent - return the current content window
* sendChromeEvent - send a chromeEvent from the browser shell
*/
let GaiaInterface = {
let ContentInterface = {
_getBrowser: function SignInToWebsiteController__getBrowser() {
return Services.wm.getMostRecentWindow("navigator:browser");
},
@ -126,40 +126,100 @@ let GaiaInterface = {
}
};
/*
* The Pipe abstracts the communcation channel between the Controller
* and the identity.js code running in the browser window.
*/
let Pipe = {
function Pipe() {
this._watchers = [];
}
/*
* communicate - launch a gaia window with certain options and
* provide a callback for handling messages.
*
* @param aRpOptions options describing the Relying Party's
* (dictionary) call, such as origin and loggedInUser.
*
* @param aGaiaOptions showUI: boolean
* (dictionary) message: name of the message to emit
* (request, logout, watch)
*
* @param aMessageCallback function to call on receipt of a
* (function) do-method message. These messages name
* a method ('login', 'logout', etc.) and
* carry optional parameters. The Pipe does
* not know what the messages mean; it is
* up to the caller to interpret them and
* act on them.
*/
communicate: function(aRpOptions, aGaiaOptions, aMessageCallback) {
log("open gaia dialog with options:", aGaiaOptions);
Pipe.prototype = {
init: function pipe_init() {
Services.obs.addObserver(this, "identity-child-process-shutdown", false);
Services.obs.addObserver(this, "identity-controller-unwatch", false);
},
uninit: function pipe_init() {
Services.obs.removeObserver(this, "identity-child-process-shutdown");
Services.obs.removeObserver(this, "identity-controller-unwatch");
},
observe: function Pipe_observe(aSubject, aTopic, aData) {
let options = {};
if (aSubject) {
options = aSubject.wrappedJSObject;
}
switch (aTopic) {
case "identity-child-process-shutdown":
log("pipe removing watchers by message manager");
this._removeWatchers(null, options.messageManager);
break;
case "identity-controller-unwatch":
log("unwatching", options.id);
this._removeWatchers(options.id, options.messageManager);
break;
}
},
_addWatcher: function Pipe__addWatcher(aId, aMm) {
log("Adding watcher with id", aId);
for (let i = 0; i < this._watchers.length; ++i) {
let watcher = this._watchers[i];
if (this._watcher.id === aId) {
watcher.count++;
return;
}
}
this._watchers.push({id: aId, count: 1, mm: aMm});
},
_removeWatchers: function Pipe__removeWatcher(aId, aMm) {
let checkId = aId !== null;
let index = -1;
for (let i = 0; i < this._watchers.length; ++i) {
let watcher = this._watchers[i];
if (watcher.mm === aMm &&
(!checkId || (checkId && watcher.id === aId))) {
index = i;
break;
}
}
if (index !== -1) {
if (checkId) {
if (--(this._watchers[index].count) === 0) {
this._watchers.splice(index, 1);
}
} else {
this._watchers.splice(index, 1);
}
}
if (this._watchers.length === 0) {
log("No more watchers; clean up persona host iframe");
let detail = {
type: kCloseIdentityDialog
};
log('telling content to close the dialog');
// tell content to close the dialog
ContentInterface.sendChromeEvent(detail);
}
},
communicate: function(aRpOptions, aContentOptions, aMessageCallback) {
let rpID = aRpOptions.id;
let rpMM = aRpOptions.mm;
if (rpMM) {
this._addWatcher(rpID, rpMM);
}
log("RP options:", aRpOptions, "\n content options:", aContentOptions);
// This content variable is injected into the scope of
// kIdentityShimFile, where it is used to access the BrowserID object
// and its internal API.
let content = GaiaInterface.getContent();
let content = ContentInterface.getContent();
let mm = null;
let uuid = getRandomId();
let self = this;
if (!content) {
log("ERROR: what the what? no content window?");
@ -178,14 +238,14 @@ let Pipe = {
removeMessageListeners();
let detail = {
type: kReceivedIdentityAssertion,
showUI: aGaiaOptions.showUI || false,
id: kReceivedIdentityAssertion + "-" + uuid,
requestId: aRpOptions.id
type: kDoneIdentityDialog,
showUI: aContentOptions.showUI || false,
id: kDoneIdentityDialog + "-" + uuid,
requestId: aRpOptions.id
};
log('telling gaia to close the dialog');
// tell gaia to close the dialog
GaiaInterface.sendChromeEvent(detail);
log('received delegate finished; telling content to close the dialog');
ContentInterface.sendChromeEvent(detail);
self._removeWatchers(rpID, rpMM);
}
content.addEventListener("mozContentEvent", function getAssertion(evt) {
@ -224,11 +284,11 @@ let Pipe = {
mm.addMessageListener(kIdentityControllerDoMethod, aMessageCallback);
mm.addMessageListener(kIdentityDelegateFinished, identityDelegateFinished);
mm.sendAsyncMessage(aGaiaOptions.message, aRpOptions);
mm.sendAsyncMessage(aContentOptions.message, aRpOptions);
}
break;
case kReceivedIdentityAssertion + '-' + uuid:
case kDoneIdentityDialog + '-' + uuid:
// Received our assertion. The message manager callbacks will handle
// communicating back to the IDService. All we have to do is remove
// this listener.
@ -242,27 +302,27 @@ let Pipe = {
});
// Tell gaia to open the identity iframe or trusty popup. The parameter
// showUI signals whether user interaction is needed. If it is, gaia will
// Tell content to open the identity iframe or trusty popup. The parameter
// showUI signals whether user interaction is needed. If it is, content will
// open a dialog; if not, a hidden iframe. In each case, BrowserID is
// available in the context.
let detail = {
type: kOpenIdentityDialog,
showUI: aGaiaOptions.showUI || false,
showUI: aContentOptions.showUI || false,
id: kOpenIdentityDialog + "-" + uuid,
requestId: aRpOptions.id
};
GaiaInterface.sendChromeEvent(detail);
ContentInterface.sendChromeEvent(detail);
}
};
/*
* The controller sits between the IdentityService used by DOMIdentity
* and a gaia process launches an (invisible) iframe or (visible)
* and a content process launches an (invisible) iframe or (visible)
* trusty UI. Using an injected js script (identity.js), the
* controller enables the gaia window to access the persona identity
* controller enables the content window to access the persona identity
* storage in the system cookie jar and send events back via the
* controller into IdentityService and DOM, and ultimately up to the
* Relying Party, which is open in a different window context.
@ -270,12 +330,12 @@ let Pipe = {
this.SignInToWebsiteController = {
/*
* Initialize the controller. To use a different gaia communication pipe,
* Initialize the controller. To use a different content communication pipe,
* such as when mocking it in tests, pass aOptions.pipe.
*/
init: function SignInToWebsiteController_init(aOptions) {
aOptions = aOptions || {};
this.pipe = aOptions.pipe || Pipe;
this.pipe = aOptions.pipe || new Pipe();
Services.obs.addObserver(this, "identity-controller-watch", false);
Services.obs.addObserver(this, "identity-controller-request", false);
Services.obs.addObserver(this, "identity-controller-logout", false);
@ -293,7 +353,7 @@ this.SignInToWebsiteController = {
if (aSubject) {
options = aSubject.wrappedJSObject;
}
switch(aTopic) {
switch (aTopic) {
case "identity-controller-watch":
this.doWatch(options);
break;
@ -320,7 +380,7 @@ this.SignInToWebsiteController = {
message = JSON.parse(message);
}
switch(message.method) {
switch (message.method) {
case "ready":
IdentityService.doReady(aRpId);
break;
@ -350,11 +410,12 @@ this.SignInToWebsiteController = {
doWatch: function SignInToWebsiteController_doWatch(aRpOptions) {
// dom prevents watch from being called twice
var gaiaOptions = {
let contentOptions = {
message: kIdentityDelegateWatch,
showUI: false
};
this.pipe.communicate(aRpOptions, gaiaOptions, this._makeDoMethodCallback(aRpOptions.id));
this.pipe.communicate(aRpOptions, contentOptions,
this._makeDoMethodCallback(aRpOptions.id));
},
/**
@ -362,12 +423,12 @@ this.SignInToWebsiteController = {
*/
doRequest: function SignInToWebsiteController_doRequest(aRpOptions) {
log("doRequest", aRpOptions);
// tell gaia to open the identity popup
var gaiaOptions = {
let contentOptions = {
message: kIdentityDelegateRequest,
showUI: true
};
this.pipe.communicate(aRpOptions, gaiaOptions, this._makeDoMethodCallback(aRpOptions.id));
this.pipe.communicate(aRpOptions, contentOptions,
this._makeDoMethodCallback(aRpOptions.id));
},
/*
@ -375,11 +436,12 @@ this.SignInToWebsiteController = {
*/
doLogout: function SignInToWebsiteController_doLogout(aRpOptions) {
log("doLogout", aRpOptions);
var gaiaOptions = {
let contentOptions = {
message: kIdentityDelegateLogout,
showUI: false
};
this.pipe.communicate(aRpOptions, gaiaOptions, this._makeDoMethodCallback(aRpOptions.id));
this.pipe.communicate(aRpOptions, contentOptions,
this._makeDoMethodCallback(aRpOptions.id));
}
};

Просмотреть файл

@ -63,7 +63,7 @@ IDPProvisioningContext.prototype = {
doError: function(msg) {
log("Provisioning ERROR: " + msg);
},
}
};
function IDPAuthenticationContext(aID, aOrigin, aTargetMM) {
@ -85,7 +85,7 @@ IDPAuthenticationContext.prototype = {
doError: function IDPAC_doError(msg) {
log("Authentication ERROR: " + msg);
},
}
};
function RPWatchContext(aOptions, aTargetMM) {
@ -152,11 +152,14 @@ this.DOMIdentity = {
case "Identity:RP:Watch":
this._watch(msg, targetMM);
break;
case "Identity:RP:Unwatch":
this._unwatch(msg, targetMM);
break;
case "Identity:RP:Request":
this._request(msg);
this._request(msg, targetMM);
break;
case "Identity:RP:Logout":
this._logout(msg);
this._logout(msg, targetMM);
break;
// IDP
case "Identity:IDP:BeginProvisioning":
@ -180,6 +183,12 @@ this.DOMIdentity = {
case "Identity:IDP:AuthenticationFailure":
this._authenticationFailure(msg);
break;
case "child-process-shutdown":
// we receive child-process-shutdown if the appliction crashes,
// including if it is crashed by the OS (killed for out-of-memory,
// for example)
this._childProcessShutdown(targetMM);
break;
}
},
@ -199,7 +208,9 @@ this.DOMIdentity = {
"Identity:IDP:RegisterCertificate", "Identity:IDP:GenKeyPair",
"Identity:IDP:BeginAuthentication",
"Identity:IDP:CompleteAuthentication",
"Identity:IDP:AuthenticationFailure"],
"Identity:IDP:AuthenticationFailure",
"Identity:RP:Unwatch",
"child-process-shutdown"],
// Private.
_init: function DOMIdentity__init() {
@ -239,6 +250,10 @@ this.DOMIdentity = {
IdentityService.RP.watch(context);
},
_unwatch: function DOMIdentity_unwatch(message, targetMM) {
IdentityService.RP.unwatch(message.id, targetMM);
},
_request: function DOMIdentity__request(message) {
IdentityService.RP.request(message.id, message);
},
@ -247,6 +262,10 @@ this.DOMIdentity = {
IdentityService.RP.logout(message.id, message.origin, message);
},
_childProcessShutdown: function DOMIdentity__childProcessShutdown(targetMM) {
IdentityService.RP.childProcessShutdown(targetMM);
},
_beginProvisioning: function DOMIdentity__beginProvisioning(message, targetMM) {
let context = new IDPProvisioningContext(message.id, message.origin,
targetMM);
@ -277,7 +296,7 @@ this.DOMIdentity = {
_authenticationFailure: function DOMIdentity__authenticationFailure(message) {
IdentityService.IDP.cancelAuthentication(message.id);
},
}
};
// Object is initialized by nsIDService.js

Просмотреть файл

@ -49,7 +49,7 @@ nsDOMIdentity.prototype = {
// Authentication
beginAuthentication: 'r',
completeAuthentication: 'r',
raiseAuthenticationFailure: 'r',
raiseAuthenticationFailure: 'r'
},
// require native events unless syntheticEventsOk is set
@ -405,7 +405,7 @@ nsDOMIdentity.prototype = {
case "Identity:RP:Watch:OnLogin":
// Do we have a watcher?
if (!this._rpWatcher) {
dump("WARNING: Received OnLogin message, but there is no RP watcher\n");
this._log("WARNING: Received OnLogin message, but there is no RP watcher");
return;
}
@ -420,7 +420,7 @@ nsDOMIdentity.prototype = {
case "Identity:RP:Watch:OnLogout":
// Do we have a watcher?
if (!this._rpWatcher) {
dump("WARNING: Received OnLogout message, but there is no RP watcher\n");
this._log("WARNING: Received OnLogout message, but there is no RP watcher");
return;
}
@ -431,7 +431,7 @@ nsDOMIdentity.prototype = {
case "Identity:RP:Watch:OnReady":
// Do we have a watcher?
if (!this._rpWatcher) {
dump("WARNING: Received OnReady message, but there is no RP watcher\n");
this._log("WARNING: Received OnReady message, but there is no RP watcher");
return;
}
@ -442,7 +442,7 @@ nsDOMIdentity.prototype = {
case "Identity:RP:Watch:OnCancel":
// Do we have a watcher?
if (!this._rpWatcher) {
dump("WARNING: Received OnCancel message, but there is no RP watcher\n");
this._log("WARNING: Received OnCancel message, but there is no RP watcher");
return;
}
@ -524,6 +524,14 @@ nsDOMIdentity.prototype = {
return message;
},
uninit: function DOMIdentity_uninit() {
this._log("nsDOMIdentity uninit()");
this._identityInternal._mm.sendAsyncMessage(
"Identity:RP:Unwatch",
{ id: this._id }
);
}
};
/**
@ -550,6 +558,8 @@ nsDOMIdentityInternal.prototype = {
return;
}
this._identity.uninit();
Services.obs.removeObserver(this, "inner-window-destroyed");
this._identity._initializeState();
this._identity = null;
@ -601,11 +611,11 @@ nsDOMIdentityInternal.prototype = {
"Identity:RP:Watch:OnCancel",
"Identity:IDP:CallBeginProvisioningCallback",
"Identity:IDP:CallGenKeyPairCallback",
"Identity:IDP:CallBeginAuthenticationCallback",
"Identity:IDP:CallBeginAuthenticationCallback"
];
this._messages.forEach((function(msgName) {
this._messages.forEach(function(msgName) {
this._mm.addMessageListener(msgName, this);
}).bind(this));
}, this);
// Setup observers so we can remove message listeners.
Services.obs.addObserver(this, "inner-window-destroyed", false);

Просмотреть файл

@ -60,8 +60,10 @@ function makeMessageObject(aRpCaller) {
}
});
if (! (options.id && options.origin)) {
let err = "id and origin required in relying-party message";
// check validity of message structure
if ((typeof options.id === 'undefined') ||
(typeof options.origin === 'undefined')) {
let err = "id and origin required in relying-party message: " + JSON.stringify(options);
reportError(err);
throw new Error(err);
}
@ -137,6 +139,21 @@ IDService.prototype = {
Services.obs.notifyObservers({wrappedJSObject: options},"identity-controller-watch", null);
},
/*
* The RP has gone away; remove handles to the hidden iframe.
* It's probable that the frame will already have been cleaned up.
*/
unwatch: function unwatch(aRpId, aTargetMM) {
let rp = this._rpFlows[aRpId];
let options = makeMessageObject({
id: aRpId,
origin: rp.origin,
messageManager: aTargetMM
});
log("sending identity-controller-unwatch for id", options.id, options.origin);
Services.obs.notifyObservers({wrappedJSObject: options}, "identity-controller-unwatch", null);
},
/**
* Initiate a login with user interaction as a result of a call to
* navigator.id.request().
@ -172,6 +189,17 @@ IDService.prototype = {
Services.obs.notifyObservers({wrappedJSObject: options}, "identity-controller-logout", null);
},
childProcessShutdown: function childProcessShutdown(messageManager) {
let options = makeMessageObject({messageManager: messageManager, id: null, origin: null});
Services.obs.notifyObservers({wrappedJSObject: options}, "identity-child-process-shutdown", null);
Object.keys(this._rpFlows).forEach(function(key) {
if (this._rpFlows[key]._mm === messageManager) {
log("child process shutdown for rp", key, "- deleting flow");
delete this._rpFlows[key];
}
}, this);
},
/*
* once the UI-and-display-logic components have received
* notifications, they call back with direct invocation of the