mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-21 09:15:35 +00:00
Merge b2g-inbound to m-c. a=merge
This commit is contained in:
commit
323fbaf43a
@ -993,5 +993,7 @@ pref("services.sync.fxaccounts.enabled", true);
|
||||
pref("identity.fxaccounts.enabled", true);
|
||||
#endif
|
||||
|
||||
pref("services.mobileid.server.uri", "http://msisdn.dev.mozaws.net");
|
||||
|
||||
// Enable mapped array buffer
|
||||
pref("dom.mapped_arraybuffer.enabled", true);
|
||||
|
@ -30,6 +30,7 @@ Cu.import('resource://gre/modules/FxAccountsMgmtService.jsm');
|
||||
#endif
|
||||
|
||||
Cu.import('resource://gre/modules/DownloadsAPI.jsm');
|
||||
Cu.import('resource://gre/modules/MobileIdentityManager.jsm');
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
|
||||
"resource://gre/modules/SystemAppProxy.jsm");
|
||||
|
@ -95,3 +95,6 @@ contract @mozilla.org/commandlinehandler/general-startup;1?type=b2goop {e30b0e13
|
||||
category command-line-handler m-b2goop @mozilla.org/commandlinehandler/general-startup;1?type=b2goop
|
||||
#endif
|
||||
|
||||
# MobileIdentityUIGlue.js
|
||||
component {83dbe26a-81f3-4a75-9541-3d0b7ca496b5} MobileIdentityUIGlue.js
|
||||
contract @mozilla.org/services/mobileid-ui-glue;1 {83dbe26a-81f3-4a75-9541-3d0b7ca496b5}
|
||||
|
68
b2g/components/ContentRequestHelper.jsm
Normal file
68
b2g/components/ContentRequestHelper.jsm
Normal file
@ -0,0 +1,68 @@
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["ContentRequestHelper"];
|
||||
|
||||
const { interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
|
||||
"@mozilla.org/uuid-generator;1",
|
||||
"nsIUUIDGenerator");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
|
||||
"resource://gre/modules/SystemAppProxy.jsm");
|
||||
|
||||
function debug(msg) {
|
||||
// dump("ContentRequestHelper ** " + msg + "\n");
|
||||
}
|
||||
|
||||
this.ContentRequestHelper = function() {
|
||||
}
|
||||
|
||||
ContentRequestHelper.prototype = {
|
||||
|
||||
contentRequest: function(aContentEventName, aChromeEventName,
|
||||
aInternalEventName, aData) {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
let id = uuidgen.generateUUID().toString();
|
||||
|
||||
SystemAppProxy.addEventListener(aContentEventName,
|
||||
function onContentEvent(result) {
|
||||
SystemAppProxy.removeEventListener(aContentEventName,
|
||||
onContentEvent);
|
||||
let msg = result.detail;
|
||||
if (!msg || !msg.id || msg.id != id) {
|
||||
deferred.reject("InternalErrorWrongContentEvent " +
|
||||
JSON.stringify(msg));
|
||||
SystemAppProxy.removeEventListener(aContentEventName,
|
||||
onContentEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
debug("Got content event " + JSON.stringify(msg));
|
||||
|
||||
if (msg.error) {
|
||||
deferred.reject(msg.error);
|
||||
} else {
|
||||
deferred.resolve(msg.result);
|
||||
}
|
||||
});
|
||||
|
||||
let detail = {
|
||||
eventName: aInternalEventName,
|
||||
id: id,
|
||||
data: aData
|
||||
};
|
||||
debug("Send chrome event " + JSON.stringify(detail));
|
||||
SystemAppProxy._sendCustomEvent(aChromeEventName, detail);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
};
|
@ -6,66 +6,26 @@
|
||||
|
||||
const { interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
|
||||
"@mozilla.org/uuid-generator;1",
|
||||
"nsIUUIDGenerator");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
|
||||
"resource://gre/modules/SystemAppProxy.jsm");
|
||||
Cu.import("resource://gre/modules/ContentRequestHelper.jsm");
|
||||
|
||||
function FxAccountsUIGlue() {
|
||||
}
|
||||
|
||||
FxAccountsUIGlue.prototype = {
|
||||
|
||||
_contentRequest: function(aEventName, aData) {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
let id = uuidgen.generateUUID().toString();
|
||||
|
||||
SystemAppProxy.addEventListener("mozFxAccountsRPContentEvent",
|
||||
function onContentEvent(result) {
|
||||
let msg = result.detail;
|
||||
if (!msg || !msg.id || msg.id != id) {
|
||||
deferred.reject("InternalErrorWrongContentEvent");
|
||||
SystemAppProxy.removeEventListener("mozFxAccountsRPContentEvent",
|
||||
onContentEvent);
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("UIGlue got content event " + JSON.stringify(msg));
|
||||
|
||||
if (msg.error) {
|
||||
deferred.reject(msg);
|
||||
} else {
|
||||
deferred.resolve(msg.result);
|
||||
}
|
||||
SystemAppProxy.removeEventListener("mozFxAccountsRPContentEvent",
|
||||
onContentEvent);
|
||||
});
|
||||
|
||||
let detail = {
|
||||
eventName: aEventName,
|
||||
id: id,
|
||||
data: aData
|
||||
};
|
||||
log.debug("Send chrome event " + JSON.stringify(detail));
|
||||
SystemAppProxy._sendCustomEvent("mozFxAccountsUnsolChromeEvent", detail);
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
__proto__: ContentRequestHelper.prototype,
|
||||
|
||||
signInFlow: function() {
|
||||
return this._contentRequest("openFlow");
|
||||
return this.contentRequest("mozFxAccountsRPContentEvent",
|
||||
"mozFxAccountsUnsolChromeEvent",
|
||||
"openFlow");
|
||||
},
|
||||
|
||||
refreshAuthentication: function(aEmail) {
|
||||
return this._contentRequest("refreshAuthentication", {
|
||||
return this.contentRequest("mozFxAccountsRPContentEvent",
|
||||
"mozFxAccountsUnsolChromeEvent",
|
||||
"refreshAuthentication", {
|
||||
email: aEmail
|
||||
});
|
||||
},
|
||||
|
161
b2g/components/MobileIdentityUIGlue.js
Normal file
161
b2g/components/MobileIdentityUIGlue.js
Normal file
@ -0,0 +1,161 @@
|
||||
/* 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/. */
|
||||
|
||||
"use strict"
|
||||
|
||||
const { interfaces: Ci, utils: Cu, classes: Cc } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/ContentRequestHelper.jsm");
|
||||
Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
|
||||
Cu.import("resource://gre/modules/MobileIdentityUIGlueCommon.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
|
||||
"@mozilla.org/uuid-generator;1",
|
||||
"nsIUUIDGenerator");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
|
||||
"resource://gre/modules/SystemAppProxy.jsm");
|
||||
|
||||
const CHROME_EVENT = "mozMobileIdChromeEvent";
|
||||
const CONTENT_EVENT = "mozMobileIdContentEvent";
|
||||
const UNSOLICITED_CONTENT_EVENT = "mozMobileIdUnsolContentEvent";
|
||||
|
||||
function MobileIdentityUIGlue() {
|
||||
SystemAppProxy.addEventListener(UNSOLICITED_CONTENT_EVENT, this);
|
||||
}
|
||||
|
||||
MobileIdentityUIGlue.prototype = {
|
||||
|
||||
__proto__: ContentRequestHelper.prototype,
|
||||
|
||||
_sendChromeEvent: function(aEventName, aData) {
|
||||
SystemAppProxy._sendCustomEvent(CHROME_EVENT, {
|
||||
eventName: aEventName,
|
||||
id: uuidgen.generateUUID().toString(),
|
||||
data: aData
|
||||
});
|
||||
},
|
||||
|
||||
_oncancel: null,
|
||||
|
||||
get oncancel() {
|
||||
return this._oncancel;
|
||||
},
|
||||
|
||||
set oncancel(aCallback) {
|
||||
this._oncancel = aCallback;
|
||||
},
|
||||
|
||||
_onresendcode: null,
|
||||
|
||||
get onresendcode() {
|
||||
return this._onresendcode;
|
||||
},
|
||||
|
||||
set onresendcode(aCallback) {
|
||||
this._onresendcode = aCallback;
|
||||
},
|
||||
|
||||
startFlow: function(aManifestURL, aIccInfo) {
|
||||
let phoneNumberInfo;
|
||||
if (aIccInfo) {
|
||||
phoneNumberInfo = [];
|
||||
for (var i = 0; i < aIccInfo.length; i++) {
|
||||
let iccInfo = aIccInfo[i];
|
||||
phoneNumberInfo.push({
|
||||
primary: iccInfo.primary,
|
||||
msisdn: iccInfo.msisdn,
|
||||
operator: iccInfo.operator,
|
||||
external: iccInfo.external,
|
||||
serviceId: iccInfo.serviceId,
|
||||
mcc: iccInfo.mcc
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return this.contentRequest(CONTENT_EVENT,
|
||||
CHROME_EVENT,
|
||||
"onpermissionrequest",
|
||||
{ phoneNumberInfo: phoneNumberInfo || [],
|
||||
manifestURL: aManifestURL })
|
||||
.then(
|
||||
(result) => {
|
||||
if (!result || !result.phoneNumber && !result.serviceId) {
|
||||
return Promise.reject(ERROR_INVALID_PROMPT_RESULT);
|
||||
}
|
||||
|
||||
let promptResult = new MobileIdentityUIGluePromptResult(
|
||||
result.phoneNumber || null,
|
||||
result.prefix || null,
|
||||
result.mcc || null,
|
||||
result.serviceId || null
|
||||
);
|
||||
return promptResult;
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
verificationCodePrompt: function(aRetriesLeft, aTimeout, aTimeLeft) {
|
||||
return this.contentRequest(CONTENT_EVENT,
|
||||
CHROME_EVENT,
|
||||
"onverificationcode",
|
||||
{ retriesLeft: aRetriesLeft,
|
||||
verificationTimeout: aTimeout,
|
||||
verificationTimeoutLeft: aTimeLeft })
|
||||
.then(
|
||||
(result) => {
|
||||
if (!result || !result.verificationCode) {
|
||||
return Promise.reject(ERROR_INVALID_VERIFICATION_CODE);
|
||||
}
|
||||
|
||||
return result.verificationCode;
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
error: function(aError) {
|
||||
log.error("UI error " + aError);
|
||||
this._sendChromeEvent("onerror", {
|
||||
error: aError
|
||||
});
|
||||
},
|
||||
|
||||
verify: function() {
|
||||
this._sendChromeEvent("verify");
|
||||
},
|
||||
|
||||
verified: function(aVerifiedPhoneNumber) {
|
||||
this._sendChromeEvent("onverified", {
|
||||
verifiedPhoneNumber: aVerifiedPhoneNumber
|
||||
});
|
||||
},
|
||||
|
||||
handleEvent: function(aEvent) {
|
||||
let msg = aEvent.detail;
|
||||
if (!msg) {
|
||||
log.warning("Got invalid event");
|
||||
return;
|
||||
}
|
||||
log.debug("Got content event ${}", msg);
|
||||
|
||||
switch(msg.eventName) {
|
||||
case 'cancel':
|
||||
this.oncancel();
|
||||
break;
|
||||
case 'resendcode':
|
||||
this.onresendcode();
|
||||
break;
|
||||
default:
|
||||
log.warning("Invalid event name");
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
classID: Components.ID("{83dbe26a-81f3-4a75-9541-3d0b7ca496b5}"),
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileIdentityUIGlue])
|
||||
};
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MobileIdentityUIGlue]);
|
@ -15,6 +15,7 @@ EXTRA_COMPONENTS += [
|
||||
'HelperAppDialog.js',
|
||||
'InterAppCommUIGlue.js',
|
||||
'MailtoProtocolHandler.js',
|
||||
'MobileIdentityUIGlue.js',
|
||||
'PaymentGlue.js',
|
||||
'ProcessGlobal.js',
|
||||
'SmsProtocolHandler.js',
|
||||
@ -43,6 +44,7 @@ if CONFIG['MOZ_UPDATER']:
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'AlertsHelper.jsm',
|
||||
'ContentRequestHelper.jsm',
|
||||
'ErrorPage.jsm',
|
||||
'SignInToWebsite.jsm',
|
||||
'SystemAppProxy.jsm',
|
||||
|
@ -19,8 +19,8 @@
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="2e5636e852a9354a5f8072b179cf16f72647cfd6"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="531cf670e485649c69746e46d567929fcd54cbc5"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="12af93123c5db55212d51fe235d39f21209a1eaa"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="8e4420c0c5c8e8c8e58a000278a7129403769f96"/>
|
||||
<project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="a819a94a572c7b32556435491ed8eaab841a95ff"/>
|
||||
|
@ -17,8 +17,8 @@
|
||||
</project>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="2e5636e852a9354a5f8072b179cf16f72647cfd6"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="531cf670e485649c69746e46d567929fcd54cbc5"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="12af93123c5db55212d51fe235d39f21209a1eaa"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="cabebb87fcd32f8596af08e6b5e80764ee0157dd"/>
|
||||
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
|
||||
|
@ -15,9 +15,9 @@
|
||||
<project name="platform_build" path="build" remote="b2g" revision="276ce45e78b09c4a4ee643646f691d22804754c1">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="2e5636e852a9354a5f8072b179cf16f72647cfd6"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="12af93123c5db55212d51fe235d39f21209a1eaa"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="531cf670e485649c69746e46d567929fcd54cbc5"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
|
@ -19,8 +19,8 @@
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="2e5636e852a9354a5f8072b179cf16f72647cfd6"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="531cf670e485649c69746e46d567929fcd54cbc5"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="12af93123c5db55212d51fe235d39f21209a1eaa"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="8e4420c0c5c8e8c8e58a000278a7129403769f96"/>
|
||||
<project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="a819a94a572c7b32556435491ed8eaab841a95ff"/>
|
||||
|
@ -17,8 +17,8 @@
|
||||
</project>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="2e5636e852a9354a5f8072b179cf16f72647cfd6"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="531cf670e485649c69746e46d567929fcd54cbc5"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="12af93123c5db55212d51fe235d39f21209a1eaa"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="cabebb87fcd32f8596af08e6b5e80764ee0157dd"/>
|
||||
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
|
||||
|
@ -4,6 +4,6 @@
|
||||
"remote": "",
|
||||
"branch": ""
|
||||
},
|
||||
"revision": "0a26eafbba1c1cf459dae8b1c44b3c8b6d08f112",
|
||||
"revision": "ce7008f6cb1d3dc7e7c2f311dc4afae35d2e55a0",
|
||||
"repo_path": "/integration/gaia-central"
|
||||
}
|
||||
|
@ -17,8 +17,8 @@
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="2e5636e852a9354a5f8072b179cf16f72647cfd6"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="531cf670e485649c69746e46d567929fcd54cbc5"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="12af93123c5db55212d51fe235d39f21209a1eaa"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
|
||||
|
@ -15,8 +15,8 @@
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="2e5636e852a9354a5f8072b179cf16f72647cfd6"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="531cf670e485649c69746e46d567929fcd54cbc5"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="12af93123c5db55212d51fe235d39f21209a1eaa"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
|
||||
|
@ -17,8 +17,8 @@
|
||||
</project>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="2e5636e852a9354a5f8072b179cf16f72647cfd6"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="531cf670e485649c69746e46d567929fcd54cbc5"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="12af93123c5db55212d51fe235d39f21209a1eaa"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="cabebb87fcd32f8596af08e6b5e80764ee0157dd"/>
|
||||
<project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
|
||||
|
@ -17,8 +17,8 @@
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="2e5636e852a9354a5f8072b179cf16f72647cfd6"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="531cf670e485649c69746e46d567929fcd54cbc5"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="12af93123c5db55212d51fe235d39f21209a1eaa"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="dbb66e540194a187326cece28ae0b51cdd500184"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="ce95d372e6d285725b96490afdaaf489ad8f9ca9"/>
|
||||
|
@ -831,6 +831,12 @@ bin/components/@DLL_PREFIX@nkgnomevfs@DLL_SUFFIX@
|
||||
@BINPATH@/components/DataStoreImpl.js
|
||||
@BINPATH@/components/dom_datastore.xpt
|
||||
|
||||
@BINPATH@/components/MobileIdentity.manifest
|
||||
@BINPATH@/components/MobileIdentity.js
|
||||
@BINPATH@/components/dom_mobileidentity.xpt
|
||||
@BINPATH@/components/MobileIdentityUIGlue.js
|
||||
@BINPATH@/components/services_mobileidentity.xpt
|
||||
|
||||
#ifdef MOZ_WEBSPEECH
|
||||
@BINPATH@/components/dom_webspeechsynth.xpt
|
||||
#endif
|
||||
|
@ -43,6 +43,9 @@
|
||||
#include "Connection.h"
|
||||
#include "mozilla/dom/Event.h" // for nsIDOMEvent::InternalDOMEvent()
|
||||
#include "nsGlobalWindow.h"
|
||||
#ifdef MOZ_B2G
|
||||
#include "nsIMobileIdentityService.h"
|
||||
#endif
|
||||
#ifdef MOZ_B2G_RIL
|
||||
#include "mozilla/dom/IccManager.h"
|
||||
#include "mozilla/dom/CellBroadcast.h"
|
||||
@ -1584,6 +1587,30 @@ Navigator::GetMozTelephony(ErrorResult& aRv)
|
||||
return mTelephony;
|
||||
}
|
||||
|
||||
#ifdef MOZ_B2G
|
||||
already_AddRefed<Promise>
|
||||
Navigator::GetMobileIdAssertion(ErrorResult& aRv)
|
||||
{
|
||||
if (!mWindow || !mWindow->GetDocShell()) {
|
||||
aRv.Throw(NS_ERROR_UNEXPECTED);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIMobileIdentityService> service =
|
||||
do_GetService("@mozilla.org/mobileidentity-service;1");
|
||||
if (!service) {
|
||||
aRv.Throw(NS_ERROR_FAILURE);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsISupports> promise;
|
||||
aRv = service->GetMobileIdAssertion(mWindow, getter_AddRefs(promise));
|
||||
|
||||
nsRefPtr<Promise> p = static_cast<Promise*>(promise.get());
|
||||
return p.forget();
|
||||
}
|
||||
#endif // MOZ_B2G
|
||||
|
||||
#ifdef MOZ_B2G_RIL
|
||||
|
||||
MobileConnectionArray*
|
||||
@ -1647,7 +1674,6 @@ Navigator::GetMozIccManager(ErrorResult& aRv)
|
||||
|
||||
return mIccManager;
|
||||
}
|
||||
|
||||
#endif // MOZ_B2G_RIL
|
||||
|
||||
#ifdef MOZ_GAMEPAD
|
||||
|
@ -215,12 +215,15 @@ public:
|
||||
systemMessageCallback* aCallback,
|
||||
ErrorResult& aRv);
|
||||
bool MozHasPendingMessage(const nsAString& aType, ErrorResult& aRv);
|
||||
#ifdef MOZ_B2G
|
||||
already_AddRefed<Promise> GetMobileIdAssertion(ErrorResult& aRv);
|
||||
#endif
|
||||
#ifdef MOZ_B2G_RIL
|
||||
MobileConnectionArray* GetMozMobileConnections(ErrorResult& aRv);
|
||||
CellBroadcast* GetMozCellBroadcast(ErrorResult& aRv);
|
||||
Voicemail* GetMozVoicemail(ErrorResult& aRv);
|
||||
IccManager* GetMozIccManager(ErrorResult& aRv);
|
||||
#endif // MOZ_B2G_RIL
|
||||
#endif // MOZ_B2G_RILi
|
||||
#ifdef MOZ_GAMEPAD
|
||||
void GetGamepads(nsTArray<nsRefPtr<Gamepad> >& aGamepads, ErrorResult& aRv);
|
||||
#endif // MOZ_GAMEPAD
|
||||
|
@ -164,7 +164,7 @@ static CINDItem sCINDItems[] = {
|
||||
#endif
|
||||
};
|
||||
|
||||
class BluetoothHfpManager::GetVolumeTask : public nsISettingsServiceCallback
|
||||
class BluetoothHfpManager::GetVolumeTask MOZ_FINAL : public nsISettingsServiceCallback
|
||||
{
|
||||
public:
|
||||
NS_DECL_ISUPPORTS
|
||||
|
11
dom/mobileid/interfaces/moz.build
Normal file
11
dom/mobileid/interfaces/moz.build
Normal file
@ -0,0 +1,11 @@
|
||||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
XPIDL_SOURCES += [
|
||||
'nsIMobileIdentityService.idl',
|
||||
]
|
||||
|
||||
XPIDL_MODULE = 'dom_mobileidentity'
|
13
dom/mobileid/interfaces/nsIMobileIdentityService.idl
Normal file
13
dom/mobileid/interfaces/nsIMobileIdentityService.idl
Normal file
@ -0,0 +1,13 @@
|
||||
/* 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/. */
|
||||
|
||||
#include "nsISupports.idl"
|
||||
|
||||
interface nsIDOMWindow;
|
||||
|
||||
[scriptable, uuid(376d3a43-a7f2-4ac0-b196-d83acb3a68cc)]
|
||||
interface nsIMobileIdentityService : nsISupports
|
||||
{
|
||||
nsISupports getMobileIdAssertion(in nsIDOMWindow window);
|
||||
};
|
7
dom/mobileid/moz.build
Normal file
7
dom/mobileid/moz.build
Normal file
@ -0,0 +1,7 @@
|
||||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
PARALLEL_DIRS += ['interfaces', 'src']
|
108
dom/mobileid/src/MobileIdentity.js
Normal file
108
dom/mobileid/src/MobileIdentity.js
Normal file
@ -0,0 +1,108 @@
|
||||
/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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/. */
|
||||
|
||||
"use strict"
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
|
||||
Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
const MOBILEIDSERVICE_CID =
|
||||
Components.ID("{6ec1806c-d61f-4724-9495-68c26d46dc53}");
|
||||
|
||||
const IPC_MSG_NAMES = ["MobileId:GetAssertion:Return:OK",
|
||||
"MobileId:GetAssertion:Return:KO"];
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
|
||||
"@mozilla.org/childprocessmessagemanager;1",
|
||||
"nsIMessageSender");
|
||||
|
||||
function MobileIdentityService() {
|
||||
}
|
||||
|
||||
MobileIdentityService.prototype = {
|
||||
__proto__: DOMRequestIpcHelper.prototype,
|
||||
|
||||
// TODO: this should be handled by DOMRequestIpcHelper. Bug 1020582
|
||||
_windows: {},
|
||||
|
||||
getMobileIdAssertion: function(aWindow) {
|
||||
log.debug("getMobileIdAssertion");
|
||||
|
||||
if (!this.init) {
|
||||
this.initDOMRequestHelper(aWindow, IPC_MSG_NAMES);
|
||||
this.init = true;
|
||||
}
|
||||
|
||||
return new aWindow.Promise(
|
||||
(resolve, reject) => {
|
||||
let promiseId = this.getPromiseResolverId({
|
||||
resolve: resolve,
|
||||
reject: reject
|
||||
});
|
||||
|
||||
this._windows[promiseId] = aWindow;
|
||||
|
||||
cpmm.sendAsyncMessage("MobileId:GetAssertion", {
|
||||
promiseId: promiseId
|
||||
}, null, aWindow.document.nodePrincipal);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
receiveMessage: function(aMessage) {
|
||||
let name = aMessage.name;
|
||||
let msg = aMessage.json;
|
||||
|
||||
log.debug("Received message " + name + ": " + JSON.stringify(msg));
|
||||
|
||||
let promiseId = msg.promiseId || msg.requestID;
|
||||
let promise = this.takePromiseResolver(promiseId);
|
||||
if (!promise) {
|
||||
log.error("Promise not found");
|
||||
return;
|
||||
}
|
||||
|
||||
let _window = this._windows[promiseId];
|
||||
delete this._windows[promiseId];
|
||||
if (!_window) {
|
||||
log.error("No window object found");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (name) {
|
||||
case "MobileId:GetAssertion:Return:OK":
|
||||
if (!msg.result) {
|
||||
promise.reject(new _window.DOMError(ERROR_INTERNAL_UNEXPECTED));
|
||||
}
|
||||
|
||||
// Return the assertion
|
||||
promise.resolve(msg.result);
|
||||
break;
|
||||
case "MobileId:GetAssertion:Return:KO":
|
||||
promise.reject(new _window.DOMError(msg.error || ERROR_UNKNOWN));
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
classID: MOBILEIDSERVICE_CID,
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileIdentityService,
|
||||
Ci.nsISupportsWeakReference,
|
||||
Ci.nsIObserver]),
|
||||
|
||||
classInfo: XPCOMUtils.generateCI({
|
||||
classID: MOBILEIDSERVICE_CID,
|
||||
contractID: "@mozilla.org/mobileidentity-service;1",
|
||||
interfaces: [Ci.nsIMobileIdentityService],
|
||||
flags: Ci.nsIClassInfo.SINGLETON
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MobileIdentityService]);
|
2
dom/mobileid/src/MobileIdentity.manifest
Normal file
2
dom/mobileid/src/MobileIdentity.manifest
Normal file
@ -0,0 +1,2 @@
|
||||
component {6ec1806c-d61f-4724-9495-68c26d46dc53} MobileIdentity.js
|
||||
contract @mozilla.org/mobileidentity-service;1 {6ec1806c-d61f-4724-9495-68c26d46dc53}
|
10
dom/mobileid/src/moz.build
Normal file
10
dom/mobileid/src/moz.build
Normal file
@ -0,0 +1,10 @@
|
||||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
EXTRA_COMPONENTS += [
|
||||
'MobileIdentity.js',
|
||||
'MobileIdentity.manifest',
|
||||
]
|
@ -113,7 +113,10 @@ if CONFIG['MOZ_NFC']:
|
||||
PARALLEL_DIRS += ['nfc']
|
||||
|
||||
if CONFIG['MOZ_B2G']:
|
||||
PARALLEL_DIRS += ['downloads']
|
||||
PARALLEL_DIRS += [
|
||||
'downloads',
|
||||
'mobileid'
|
||||
]
|
||||
|
||||
if CONFIG['MOZ_B2G_BT_API_V2']:
|
||||
PARALLEL_DIRS += ['bluetooth2']
|
||||
|
@ -3,8 +3,21 @@
|
||||
|
||||
MARIONETTE_CONTEXT = "chrome";
|
||||
|
||||
const SETTINGS_KEY_DATA_ENABLED = "ril.data.enabled";
|
||||
const SETTINGS_KEY_DATA_APN_SETTINGS = "ril.data.apnSettings";
|
||||
|
||||
const TOPIC_CONNECTION_STATE_CHANGED = "network-connection-state-changed";
|
||||
const TOPIC_INTERFACE_STATE_CHANGED = "network-interface-state-changed";
|
||||
const TOPIC_NETWORK_ACTIVE_CHANGED = "network-active-changed";
|
||||
|
||||
let Promise = Cu.import("resource://gre/modules/Promise.jsm").Promise;
|
||||
|
||||
let ril = Cc["@mozilla.org/ril;1"].getService(Ci.nsIRadioInterfaceLayer);
|
||||
ok(ril, "ril.constructor is " + ril.constructor);
|
||||
|
||||
let radioInterface = ril.getRadioInterface(0);
|
||||
ok(radioInterface, "radioInterface.constructor is " + radioInterface.constrctor);
|
||||
|
||||
/**
|
||||
* Wrap DOMRequest onsuccess/onerror events to Promise resolve/reject.
|
||||
*
|
||||
@ -116,6 +129,103 @@ function waitForObserverEvent(aTopic) {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
let mobileTypeMapping = {
|
||||
"default": Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE,
|
||||
"mms": Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_MMS,
|
||||
"supl": Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_SUPL,
|
||||
"ims": Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_IMS,
|
||||
"dun": Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_DUN
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the default data connection enabling state, wait for
|
||||
* "network-connection-state-changed" event and verify state.
|
||||
*
|
||||
* Fulfill params: (none)
|
||||
*
|
||||
* @param aEnabled
|
||||
* A boolean state.
|
||||
*
|
||||
* @return A deferred promise.
|
||||
*/
|
||||
function setDataEnabledAndWait(aEnabled) {
|
||||
let promises = [];
|
||||
promises.push(waitForObserverEvent(TOPIC_CONNECTION_STATE_CHANGED)
|
||||
.then(function(aSubject) {
|
||||
ok(aSubject instanceof Ci.nsIRilNetworkInterface,
|
||||
"subject should be an instance of nsIRILNetworkInterface");
|
||||
is(aSubject.type, Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE,
|
||||
"subject.type should be " + Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE);
|
||||
is(aSubject.state,
|
||||
aEnabled ? Ci.nsINetworkInterface.NETWORK_STATE_CONNECTED
|
||||
: Ci.nsINetworkInterface.NETWORK_STATE_DISCONNECTED,
|
||||
"subject.state should be " + aEnabled ? "CONNECTED" : "DISCONNECTED");
|
||||
}));
|
||||
promises.push(setSettings(SETTINGS_KEY_DATA_ENABLED, aEnabled));
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup a certain type of data connection, wait for
|
||||
* "network-connection-state-changed" event and verify state.
|
||||
*
|
||||
* Fulfill params: (none)
|
||||
*
|
||||
* @param aType
|
||||
* The string of the type of data connection to setup.
|
||||
*
|
||||
* @return A deferred promise.
|
||||
*/
|
||||
function setupDataCallAndWait(aType) {
|
||||
log("setupDataCallAndWait: " + aType);
|
||||
|
||||
let promises = [];
|
||||
promises.push(waitForObserverEvent(TOPIC_CONNECTION_STATE_CHANGED)
|
||||
.then(function(aSubject) {
|
||||
let networkType = mobileTypeMapping[aType];
|
||||
ok(aSubject instanceof Ci.nsIRilNetworkInterface,
|
||||
"subject should be an instance of nsIRILNetworkInterface");
|
||||
is(aSubject.type, networkType,
|
||||
"subject.type should be " + networkType);
|
||||
is(aSubject.state, Ci.nsINetworkInterface.NETWORK_STATE_CONNECTED,
|
||||
"subject.state should be CONNECTED");
|
||||
}));
|
||||
promises.push(radioInterface.setupDataCallByType(aType));
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate a certain type of data connection, wait for
|
||||
* "network-connection-state-changed" event and verify state.
|
||||
*
|
||||
* Fulfill params: (none)
|
||||
*
|
||||
* @param aType
|
||||
* The string of the type of data connection to deactivate.
|
||||
*
|
||||
* @return A deferred promise.
|
||||
*/
|
||||
function deactivateDataCallAndWait(aType) {
|
||||
log("deactivateDataCallAndWait: " + aType);
|
||||
|
||||
let promises = [];
|
||||
promises.push(waitForObserverEvent(TOPIC_CONNECTION_STATE_CHANGED)
|
||||
.then(function(aSubject) {
|
||||
let networkType = mobileTypeMapping[aType];
|
||||
ok(aSubject instanceof Ci.nsIRilNetworkInterface,
|
||||
"subject should be an instance of nsIRILNetworkInterface");
|
||||
is(aSubject.type, networkType,
|
||||
"subject.type should be " + networkType);
|
||||
is(aSubject.state, Ci.nsINetworkInterface.NETWORK_STATE_DISCONNECTED,
|
||||
"subject.state should be DISCONNECTED");
|
||||
}));
|
||||
promises.push(radioInterface.deactivateDataCallByType(aType));
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic test routine helper.
|
||||
*
|
||||
|
@ -11,3 +11,4 @@ disabled = Bug 808783
|
||||
[test_dsds_numRadioInterfaces.js]
|
||||
[test_data_connection.js]
|
||||
[test_network_active_changed.js]
|
||||
[test_multiple_data_connection.js]
|
||||
|
@ -4,18 +4,6 @@
|
||||
MARIONETTE_TIMEOUT = 60000;
|
||||
MARIONETTE_HEAD_JS = "head.js";
|
||||
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
|
||||
const DATA_KEY = "ril.data.enabled";
|
||||
const APN_KEY = "ril.data.apnSettings";
|
||||
const TOPIC_CONNECTION_STATE_CHANGED = "network-connection-state-changed";
|
||||
|
||||
let ril = Cc["@mozilla.org/ril;1"].getService(Ci.nsIRadioInterfaceLayer);
|
||||
ok(ril, "ril.constructor is " + ril.constructor);
|
||||
|
||||
let radioInterface = ril.getRadioInterface(0);
|
||||
ok(radioInterface, "radioInterface.constructor is " + radioInterface.constrctor);
|
||||
|
||||
function setEmulatorAPN() {
|
||||
let apn = [
|
||||
[{"carrier":"T-Mobile US",
|
||||
@ -24,57 +12,7 @@ function setEmulatorAPN() {
|
||||
"types":["default","supl","mms","ims","dun"]}]
|
||||
];
|
||||
|
||||
return setSettings(APN_KEY, apn);
|
||||
}
|
||||
|
||||
function setupDataCallAndWait(type, networkType) {
|
||||
let promises = [];
|
||||
promises.push(waitForObserverEvent(TOPIC_CONNECTION_STATE_CHANGED));
|
||||
promises.push(radioInterface.setupDataCallByType(type));
|
||||
|
||||
return Promise.all(promises).then(function(results) {
|
||||
let subject = results[0];
|
||||
ok(subject instanceof Ci.nsIRilNetworkInterface,
|
||||
"subject should be an instance of nsIRILNetworkInterface");
|
||||
is(subject.type, networkType,
|
||||
"subject.type should be " + networkType);
|
||||
is(subject.state, Ci.nsINetworkInterface.NETWORK_STATE_CONNECTED,
|
||||
"subject.state should be CONNECTED");
|
||||
});
|
||||
}
|
||||
|
||||
function deactivateDataCallAndWait(type, networkType) {
|
||||
let promises = [];
|
||||
promises.push(waitForObserverEvent(TOPIC_CONNECTION_STATE_CHANGED));
|
||||
promises.push(radioInterface.deactivateDataCallByType(type));
|
||||
|
||||
return Promise.all(promises).then(function(results) {
|
||||
let subject = results[0];
|
||||
ok(subject instanceof Ci.nsIRilNetworkInterface,
|
||||
"subject should be an instance of nsIRILNetworkInterface");
|
||||
is(subject.type, networkType,
|
||||
"subject.type should be " + networkType);
|
||||
is(subject.state, Ci.nsINetworkInterface.NETWORK_STATE_DISCONNECTED,
|
||||
"subject.state should be DISCONNECTED");
|
||||
});
|
||||
}
|
||||
|
||||
function setDataEnabledAndWait(enabled) {
|
||||
let promises = [];
|
||||
promises.push(waitForObserverEvent(TOPIC_CONNECTION_STATE_CHANGED));
|
||||
promises.push(setSettings(DATA_KEY, enabled));
|
||||
|
||||
return Promise.all(promises).then(function(results) {
|
||||
let subject = results[0];
|
||||
ok(subject instanceof Ci.nsIRilNetworkInterface,
|
||||
"subject should be an instance of nsIRILNetworkInterface");
|
||||
is(subject.type, Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE,
|
||||
"subject.type should be " + Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE);
|
||||
is(subject.state,
|
||||
enabled ? Ci.nsINetworkInterface.NETWORK_STATE_CONNECTED
|
||||
: Ci.nsINetworkInterface.NETWORK_STATE_DISCONNECTED,
|
||||
"subject.state should be " + enabled ? "CONNECTED" : "DISCONNECTED");
|
||||
});
|
||||
return setSettings(SETTINGS_KEY_DATA_APN_SETTINGS, apn);
|
||||
}
|
||||
|
||||
// Test initial State
|
||||
@ -82,7 +20,7 @@ function testInitialState() {
|
||||
log("= testInitialState =");
|
||||
|
||||
// Data should be off before starting any test.
|
||||
return getSettings(DATA_KEY)
|
||||
return getSettings(SETTINGS_KEY_DATA_ENABLED)
|
||||
.then(value => {
|
||||
is(value, false, "Data must be off");
|
||||
});
|
||||
@ -105,20 +43,12 @@ function testNonDefaultDataConnection() {
|
||||
function doTestNonDefaultDataConnection(type) {
|
||||
log("doTestNonDefaultDataConnection: " + type);
|
||||
|
||||
let typeMapping = {
|
||||
"mms": Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_MMS,
|
||||
"supl": Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_SUPL,
|
||||
"ims": Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_IMS,
|
||||
"dun": Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE_DUN
|
||||
};
|
||||
let networkType = typeMapping[type];
|
||||
|
||||
return setupDataCallAndWait(type, networkType)
|
||||
.then(() => deactivateDataCallAndWait(type, networkType));
|
||||
return setupDataCallAndWait(type)
|
||||
.then(() => deactivateDataCallAndWait(type));
|
||||
}
|
||||
|
||||
let currentApn;
|
||||
return getSettings(APN_KEY)
|
||||
return getSettings(SETTINGS_KEY_DATA_APN_SETTINGS)
|
||||
.then(value => {
|
||||
currentApn = value;
|
||||
})
|
||||
@ -128,7 +58,7 @@ function testNonDefaultDataConnection() {
|
||||
.then(() => doTestNonDefaultDataConnection("ims"))
|
||||
.then(() => doTestNonDefaultDataConnection("dun"))
|
||||
// Restore APN settings
|
||||
.then(() => setSettings(APN_KEY, currentApn));
|
||||
.then(() => setSettings(SETTINGS_KEY_DATA_APN_SETTINGS, currentApn));
|
||||
}
|
||||
|
||||
// Start test
|
||||
|
@ -0,0 +1,88 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
MARIONETTE_TIMEOUT = 60000;
|
||||
MARIONETTE_HEAD_JS = "head.js";
|
||||
|
||||
// Must sync with hardware/ril/reference-ril/reference-ril.c
|
||||
const MAX_DATA_CONTEXTS = 4;
|
||||
|
||||
function setEmulatorAPN() {
|
||||
// Use different apn for each network type.
|
||||
let apn = [[ { "carrier":"T-Mobile US",
|
||||
"apn":"epc1.tmobile.com",
|
||||
"types":["default"] },
|
||||
{ "carrier":"T-Mobile US",
|
||||
"apn":"epc2.tmobile.com",
|
||||
"mmsc":"http://mms.msg.eng.t-mobile.com/mms/wapenc",
|
||||
"types":["mms"] },
|
||||
{ "carrier":"T-Mobile US",
|
||||
"apn":"epc3.tmobile.com",
|
||||
"types":["supl"] },
|
||||
{ "carrier":"T-Mobile US",
|
||||
"apn":"epc4.tmobile.com",
|
||||
"types":["ims"] },
|
||||
{ "carrier":"T-Mobile US",
|
||||
"apn":"epc5.tmobile.com",
|
||||
"types":["dun"] }]];
|
||||
|
||||
return setSettings(SETTINGS_KEY_DATA_APN_SETTINGS, apn);
|
||||
}
|
||||
|
||||
// Test initial State
|
||||
function testInitialState() {
|
||||
log("= testInitialState =");
|
||||
|
||||
// Data should be off before starting any test.
|
||||
return getSettings(SETTINGS_KEY_DATA_ENABLED)
|
||||
.then(value => {
|
||||
is(value, false, "Data must be off");
|
||||
});
|
||||
}
|
||||
|
||||
function testSetupConcurrentDataCalls() {
|
||||
log("= testSetupConcurrentDataCalls =");
|
||||
|
||||
let promise = Promise.resolve();
|
||||
let types = Object.keys(mobileTypeMapping);
|
||||
// Skip default mobile type.
|
||||
for (let i = 1; i < MAX_DATA_CONTEXTS; i++) {
|
||||
let type = types[i];
|
||||
promise = promise.then(() => setupDataCallAndWait(type));
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
function testDeactivateConcurrentDataCalls() {
|
||||
log("= testDeactivateConcurrentDataCalls =");
|
||||
|
||||
let promise = Promise.resolve();
|
||||
let types = Object.keys(mobileTypeMapping);
|
||||
// Skip default mobile type.
|
||||
for (let i = 1; i < MAX_DATA_CONTEXTS; i++) {
|
||||
let type = types[i];
|
||||
promise = promise.then(() => deactivateDataCallAndWait(type));
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
// Start test
|
||||
startTestBase(function() {
|
||||
|
||||
let origApnSettings;
|
||||
return testInitialState()
|
||||
.then(() => getSettings(SETTINGS_KEY_DATA_APN_SETTINGS))
|
||||
.then(value => {
|
||||
origApnSettings = value;
|
||||
})
|
||||
.then(() => setEmulatorAPN())
|
||||
.then(() => setDataEnabledAndWait(true))
|
||||
.then(() => testSetupConcurrentDataCalls())
|
||||
.then(() => testDeactivateConcurrentDataCalls())
|
||||
.then(() => setDataEnabledAndWait(false))
|
||||
.then(() => {
|
||||
if (origApnSettings) {
|
||||
return setSettings(SETTINGS_KEY_DATA_APN_SETTINGS, origApnSettings);
|
||||
}
|
||||
});
|
||||
});
|
@ -4,9 +4,6 @@
|
||||
MARIONETTE_TIMEOUT = 60000;
|
||||
MARIONETTE_HEAD_JS = "head.js";
|
||||
|
||||
const SETTINGS_KEY_DATA_ENABLED = "ril.data.enabled";
|
||||
const TOPIC_NETWORK_ACTIVE_CHANGED = "network-active-changed";
|
||||
|
||||
let networkManager =
|
||||
Cc["@mozilla.org/network/manager;1"].getService(Ci.nsINetworkManager);
|
||||
ok(networkManager,
|
||||
|
@ -151,6 +151,15 @@ callback interface MozIdleObserver {
|
||||
void onactive();
|
||||
};
|
||||
|
||||
#ifdef MOZ_B2G
|
||||
[NoInterfaceObject]
|
||||
interface NavigatorMobileId {
|
||||
[Throws, NewObject]
|
||||
Promise getMobileIdAssertion();
|
||||
};
|
||||
Navigator implements NavigatorMobileId;
|
||||
#endif // MOZ_B2G
|
||||
|
||||
// nsIDOMNavigator
|
||||
partial interface Navigator {
|
||||
[Throws]
|
||||
|
15
layout/base/tests/marionette/manifest.ini
Normal file
15
layout/base/tests/marionette/manifest.ini
Normal file
@ -0,0 +1,15 @@
|
||||
[DEFAULT]
|
||||
; true if the test requires an emulator, otherwise false
|
||||
qemu = false
|
||||
|
||||
; true if the test is compatible with the browser, otherwise false
|
||||
browser = true
|
||||
|
||||
; true if the test is compatible with b2g, otherwise false
|
||||
b2g = true
|
||||
|
||||
; true if the test should be skipped
|
||||
skip = false
|
||||
|
||||
[test_touchcaret.py]
|
||||
b2g = false ; Bug 1020261
|
307
layout/base/tests/marionette/test_touchcaret.py
Normal file
307
layout/base/tests/marionette/test_touchcaret.py
Normal file
@ -0,0 +1,307 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 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/.
|
||||
|
||||
import string
|
||||
|
||||
from by import By
|
||||
from marionette import Actions
|
||||
from marionette_test import MarionetteTestCase
|
||||
|
||||
|
||||
class TouchCaretTest(MarionetteTestCase):
|
||||
_input_selector = (By.ID, 'input')
|
||||
_textarea_selector = (By.ID, 'textarea')
|
||||
_contenteditable_selector = (By.ID, 'contenteditable')
|
||||
|
||||
def setUp(self):
|
||||
# Code to execute before a tests are run.
|
||||
MarionetteTestCase.setUp(self)
|
||||
self.actions = Actions(self.marionette)
|
||||
|
||||
def openTestHtml(self, enabled=True):
|
||||
'''Open html for testing and locate elements, and enable/disable touch
|
||||
caret.'''
|
||||
self.marionette.execute_script(
|
||||
'SpecialPowers.setBoolPref("touchcaret.enabled", %s);' %
|
||||
('true' if enabled else 'false'))
|
||||
|
||||
test_html = self.marionette.absolute_url('test_touchcaret.html')
|
||||
self.marionette.navigate(test_html)
|
||||
|
||||
self._input = self.marionette.find_element(*self._input_selector)
|
||||
self._textarea = self.marionette.find_element(*self._textarea_selector)
|
||||
self._contenteditable = self.marionette.find_element(*self._contenteditable_selector)
|
||||
|
||||
def is_input_or_textarea(self, element):
|
||||
'''Return True if element is either <input> or <textarea>'''
|
||||
return element.tag_name in ('input', 'textarea')
|
||||
|
||||
def get_js_selection_cmd(self, element):
|
||||
'''Return a command snippet to get selection object.
|
||||
|
||||
If the element is <input> or <textarea>, return the selection object
|
||||
associated with it. Otherwise, return the current selection object.
|
||||
|
||||
Note: "element" must be provided as the first argument to
|
||||
execute_script().
|
||||
|
||||
'''
|
||||
if self.is_input_or_textarea(element):
|
||||
# We must unwrap sel so that DOMRect could be returned to Python
|
||||
# side.
|
||||
return '''var sel = SpecialPowers.wrap(arguments[0]).editor.selection;
|
||||
sel = SpecialPowers.unwrap(sel);'''
|
||||
else:
|
||||
return '''var sel = window.getSelection();'''
|
||||
|
||||
def caret_rect(self, element):
|
||||
'''Return the caret's DOMRect object.
|
||||
|
||||
If the element is either <input> or <textarea>, return the caret's
|
||||
DOMRect within the element. Otherwise, return the DOMRect of the
|
||||
current selected caret.
|
||||
|
||||
'''
|
||||
cmd = self.get_js_selection_cmd(element) +\
|
||||
'''return sel.getRangeAt(0).getClientRects()[0];'''
|
||||
return self.marionette.execute_script(cmd, script_args=[element])
|
||||
|
||||
def caret_location(self, element):
|
||||
'''Return caret's center location by the number of characters offset
|
||||
within the given element.
|
||||
|
||||
Return (x, y) coordinates of the caret's center by the number of
|
||||
characters offset relative to the top left-hand corner of the given
|
||||
element.
|
||||
|
||||
'''
|
||||
rect = self.caret_rect(element)
|
||||
x = rect['left'] + rect['width'] / 2.0 - element.location['x']
|
||||
y = rect['top'] + rect['height'] / 2.0 - element.location['y']
|
||||
return x, y
|
||||
|
||||
def touch_caret_location(self, element):
|
||||
'''Return touch caret's location (based on current caret location).
|
||||
|
||||
Return (x, y) coordinates of the touch caret's tip relative to the top
|
||||
left-hand corner of the given element.
|
||||
|
||||
'''
|
||||
rect = self.caret_rect(element)
|
||||
x = rect['left'] - element.location['x']
|
||||
|
||||
# Touch caret's tip is below the bottom of the caret. Add 5px to y
|
||||
# should be sufficient to locate it.
|
||||
y = rect['bottom'] + 5 - element.location['y']
|
||||
|
||||
return x, y
|
||||
|
||||
def move_caret_by_offset(self, element, offset, backward=False):
|
||||
'''Move caret in the element by offset.'''
|
||||
cmd = self.get_js_selection_cmd(element) +\
|
||||
'''sel.modify("move", arguments[1], "character");'''
|
||||
direction = 'backward' if backward else 'forward'
|
||||
|
||||
for i in range(offset):
|
||||
self.marionette.execute_script(
|
||||
cmd, script_args=[element, direction])
|
||||
|
||||
def move_caret_to_front(self, element):
|
||||
if self.is_input_or_textarea(element):
|
||||
cmd = '''arguments[0].setSelectionRange(0, 0);'''
|
||||
else:
|
||||
cmd = '''var sel = window.getSelection();
|
||||
sel.collapse(arguments[0].firstChild, 0);'''
|
||||
|
||||
self.marionette.execute_script(cmd, script_args=[element])
|
||||
|
||||
def move_caret_to_end(self, element):
|
||||
if self.is_input_or_textarea(element):
|
||||
cmd = '''var len = arguments[0].value.length;
|
||||
arguments[0].setSelectionRange(len, len);'''
|
||||
else:
|
||||
cmd = '''var sel = window.getSelection();
|
||||
sel.collapse(arguments[0].lastChild, arguments[0].lastChild.length);'''
|
||||
|
||||
self.marionette.execute_script(cmd, script_args=[element])
|
||||
|
||||
def get_content(self, element):
|
||||
'''Return the content of the element.'''
|
||||
if self.is_input_or_textarea(element):
|
||||
return element.get_attribute('value')
|
||||
else:
|
||||
return element.text
|
||||
|
||||
def _test_move_caret_to_the_right_by_one_character(self, el, assertFunc):
|
||||
content_to_add = '!'
|
||||
target_content = self.get_content(el)
|
||||
target_content = target_content[:1] + content_to_add + target_content[1:]
|
||||
|
||||
# Get touch caret (x, y) at position 1 and 2.
|
||||
self.move_caret_to_front(el)
|
||||
caret0_x, caret0_y = self.caret_location(el)
|
||||
touch_caret0_x, touch_caret0_y = self.touch_caret_location(el)
|
||||
self.move_caret_by_offset(el, 1)
|
||||
touch_caret1_x, touch_caret1_y = self.touch_caret_location(el)
|
||||
|
||||
# Tap the front of the input to make touch caret appear.
|
||||
el.tap(caret0_x, caret0_y)
|
||||
|
||||
# Move touch caret
|
||||
self.actions.flick(el, touch_caret0_x, touch_caret0_y,
|
||||
touch_caret1_x, touch_caret1_y).perform()
|
||||
|
||||
el.send_keys(content_to_add)
|
||||
assertFunc(target_content, self.get_content(el))
|
||||
|
||||
def _test_move_caret_to_end_by_dragging_touch_caret_to_bottom_right_corner(self, el, assertFunc):
|
||||
content_to_add = '!'
|
||||
target_content = self.get_content(el) + content_to_add
|
||||
|
||||
# Tap the front of the input to make touch caret appear.
|
||||
self.move_caret_to_front(el)
|
||||
el.tap(*self.caret_location(el))
|
||||
|
||||
# Move touch caret to the bottom-right corner of the element.
|
||||
src_x, src_y = self.touch_caret_location(el)
|
||||
dest_x, dest_y = el.size['width'], el.size['height']
|
||||
self.actions.flick(el, src_x, src_y, dest_x, dest_y).perform()
|
||||
|
||||
el.send_keys(content_to_add)
|
||||
assertFunc(target_content, self.get_content(el))
|
||||
|
||||
def _test_move_caret_to_front_by_dragging_touch_caret_to_top_left_corner(self, el, assertFunc):
|
||||
content_to_add = '!'
|
||||
target_content = content_to_add + self.get_content(el)
|
||||
|
||||
# Tap to make touch caret appear. Note: it's strange that when the caret
|
||||
# is at the end, the rect of the caret in <textarea> cannot be obtained.
|
||||
# A bug perhaps.
|
||||
self.move_caret_to_end(el)
|
||||
self.move_caret_by_offset(el, 1, backward=True)
|
||||
el.tap(*self.caret_location(el))
|
||||
|
||||
# Move touch caret to the top-left corner of the input box.
|
||||
src_x, src_y = self.touch_caret_location(el)
|
||||
dest_x, dest_y = 0, 0
|
||||
self.actions.flick(el, src_x, src_y, dest_x, dest_y).perform()
|
||||
|
||||
el.send_keys(content_to_add)
|
||||
assertFunc(target_content, self.get_content(el))
|
||||
|
||||
def _test_touch_caret_timeout_by_dragging_it_to_top_left_corner_after_timout(self, el, assertFunc):
|
||||
content_to_add = '!'
|
||||
non_target_content = content_to_add + self.get_content(el)
|
||||
|
||||
# Get touch caret timeout in millisecond, and convert it to second.
|
||||
timeout = self.marionette.execute_script(
|
||||
'return SpecialPowers.getIntPref("touchcaret.expiration.time");')
|
||||
timeout /= 1000.0
|
||||
|
||||
# Tap to make touch caret appear. Note: it's strange that when the caret
|
||||
# is at the end, the rect of the caret in <textarea> cannot be obtained.
|
||||
# A bug perhaps.
|
||||
self.move_caret_to_end(el)
|
||||
self.move_caret_by_offset(el, 1, backward=True)
|
||||
el.tap(*self.caret_location(el))
|
||||
|
||||
# Wait until touch caret disappears, then pretend to move it to the
|
||||
# top-left corner of the input box.
|
||||
src_x, src_y = self.touch_caret_location(el)
|
||||
dest_x, dest_y = 0, 0
|
||||
self.actions.wait(timeout).flick(el, src_x, src_y, dest_x, dest_y).perform()
|
||||
|
||||
el.send_keys(content_to_add)
|
||||
assertFunc(non_target_content, self.get_content(el))
|
||||
|
||||
########################################################################
|
||||
# <input> test cases with touch caret enabled
|
||||
########################################################################
|
||||
def test_input_move_caret_to_the_right_by_one_character(self):
|
||||
self.openTestHtml(enabled=True)
|
||||
self._test_move_caret_to_the_right_by_one_character(self._input, self.assertEqual)
|
||||
|
||||
def test_input_move_caret_to_end_by_dragging_touch_caret_to_bottom_right_corner(self):
|
||||
self.openTestHtml(enabled=True)
|
||||
self._test_move_caret_to_end_by_dragging_touch_caret_to_bottom_right_corner(self._input, self.assertEqual)
|
||||
|
||||
def test_input_move_caret_to_front_by_dragging_touch_caret_to_top_left_corner(self):
|
||||
self.openTestHtml(enabled=True)
|
||||
self._test_move_caret_to_front_by_dragging_touch_caret_to_top_left_corner(self._input, self.assertEqual)
|
||||
|
||||
def test_input_touch_caret_timeout(self):
|
||||
self.openTestHtml(enabled=True)
|
||||
self._test_touch_caret_timeout_by_dragging_it_to_top_left_corner_after_timout(self._input, self.assertNotEqual)
|
||||
|
||||
########################################################################
|
||||
# <input> test cases with touch caret disabled
|
||||
########################################################################
|
||||
def test_input_move_caret_to_the_right_by_one_character_disabled(self):
|
||||
self.openTestHtml(enabled=False)
|
||||
self._test_move_caret_to_the_right_by_one_character(self._input, self.assertNotEqual)
|
||||
|
||||
def test_input_move_caret_to_front_by_dragging_touch_caret_to_top_left_corner_disabled(self):
|
||||
self.openTestHtml(enabled=False)
|
||||
self._test_move_caret_to_front_by_dragging_touch_caret_to_top_left_corner(self._input, self.assertNotEqual)
|
||||
|
||||
########################################################################
|
||||
# <textarea> test cases with touch caret enabled
|
||||
########################################################################
|
||||
def test_textarea_move_caret_to_the_right_by_one_character(self):
|
||||
self.openTestHtml(enabled=True)
|
||||
self._test_move_caret_to_the_right_by_one_character(self._textarea, self.assertEqual)
|
||||
|
||||
def test_textarea_move_caret_to_end_by_dragging_touch_caret_to_bottom_right_corner(self):
|
||||
self.openTestHtml(enabled=True)
|
||||
self._test_move_caret_to_end_by_dragging_touch_caret_to_bottom_right_corner(self._textarea, self.assertEqual)
|
||||
|
||||
def test_textarea_move_caret_to_front_by_dragging_touch_caret_to_top_left_corner(self):
|
||||
self.openTestHtml(enabled=True)
|
||||
self._test_move_caret_to_front_by_dragging_touch_caret_to_top_left_corner(self._textarea, self.assertEqual)
|
||||
|
||||
def test_textarea_touch_caret_timeout(self):
|
||||
self.openTestHtml(enabled=True)
|
||||
self._test_touch_caret_timeout_by_dragging_it_to_top_left_corner_after_timout(self._textarea, self.assertNotEqual)
|
||||
|
||||
########################################################################
|
||||
# <textarea> test cases with touch caret disabled
|
||||
########################################################################
|
||||
def test_textarea_move_caret_to_the_right_by_one_character_disabled(self):
|
||||
self.openTestHtml(enabled=False)
|
||||
self._test_move_caret_to_the_right_by_one_character(self._textarea, self.assertNotEqual)
|
||||
|
||||
def test_textarea_move_caret_to_front_by_dragging_touch_caret_to_top_left_corner_disabled(self):
|
||||
self.openTestHtml(enabled=False)
|
||||
self._test_move_caret_to_front_by_dragging_touch_caret_to_top_left_corner(self._textarea, self.assertNotEqual)
|
||||
|
||||
########################################################################
|
||||
# <div> contenteditable test cases with touch caret enabled
|
||||
########################################################################
|
||||
def test_contenteditable_move_caret_to_the_right_by_one_character(self):
|
||||
self.openTestHtml(enabled=True)
|
||||
self._test_move_caret_to_the_right_by_one_character(self._contenteditable, self.assertEqual)
|
||||
|
||||
def test_contenteditable_move_caret_to_end_by_dragging_touch_caret_to_bottom_right_corner(self):
|
||||
self.openTestHtml(enabled=True)
|
||||
self._test_move_caret_to_end_by_dragging_touch_caret_to_bottom_right_corner(self._contenteditable, self.assertEqual)
|
||||
|
||||
def test_contenteditable_move_caret_to_front_by_dragging_touch_caret_to_top_left_corner(self):
|
||||
self.openTestHtml(enabled=True)
|
||||
self._test_move_caret_to_front_by_dragging_touch_caret_to_top_left_corner(self._contenteditable, self.assertEqual)
|
||||
|
||||
def test_contenteditable_touch_caret_timeout(self):
|
||||
self.openTestHtml(enabled=True)
|
||||
self._test_touch_caret_timeout_by_dragging_it_to_top_left_corner_after_timout(self._contenteditable, self.assertNotEqual)
|
||||
|
||||
########################################################################
|
||||
# <div> contenteditable test cases with touch caret disabled
|
||||
########################################################################
|
||||
def test_contenteditable_move_caret_to_the_right_by_one_character_disabled(self):
|
||||
self.openTestHtml(enabled=False)
|
||||
self._test_move_caret_to_the_right_by_one_character(self._contenteditable, self.assertNotEqual)
|
||||
|
||||
def test_contenteditable_move_caret_to_front_by_dragging_touch_caret_to_top_left_corner_disabled(self):
|
||||
self.openTestHtml(enabled=False)
|
||||
self._test_move_caret_to_front_by_dragging_touch_caret_to_top_left_corner(self._contenteditable, self.assertNotEqual)
|
@ -28,12 +28,49 @@ this.EXPORTED_SYMBOLS = ["HawkClient"];
|
||||
|
||||
const {interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
Cu.import("resource://services-common/hawkrequest.js");
|
||||
Cu.import("resource://services-common/observers.js");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
// loglevel should be one of "Fatal", "Error", "Warn", "Info", "Config",
|
||||
// "Debug", "Trace" or "All". If none is specified, "Error" will be used by
|
||||
// default.
|
||||
const PREF_LOG_LEVEL = "services.hawk.loglevel";
|
||||
|
||||
// A pref that can be set so "sensitive" information (eg, personally
|
||||
// identifiable info, credentials, etc) will be logged.
|
||||
const PREF_LOG_SENSITIVE_DETAILS = "services.hawk.log.sensitive";
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "log", function() {
|
||||
let log = Log.repository.getLogger("Hawk");
|
||||
log.addAppender(new Log.DumpAppender());
|
||||
log.level = Log.Level.Error;
|
||||
try {
|
||||
let level =
|
||||
Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING
|
||||
&& Services.prefs.getCharPref(PREF_LOG_LEVEL);
|
||||
log.level = Log.Level[level] || Log.Level.Error;
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
}
|
||||
|
||||
return log;
|
||||
});
|
||||
|
||||
// A boolean to indicate if personally identifiable information (or anything
|
||||
// else sensitive, such as credentials) should be logged.
|
||||
XPCOMUtils.defineLazyGetter(this, 'logPII', function() {
|
||||
try {
|
||||
return Services.prefs.getBoolPref(PREF_LOG_SENSITIVE_DETAILS);
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* A general purpose client for making HAWK authenticated requests to a single
|
||||
@ -232,7 +269,7 @@ this.HawkClient.prototype = {
|
||||
|
||||
// Given an optional header value, notify that a backoff has been requested.
|
||||
_maybeNotifyBackoff: function (response, headerName) {
|
||||
if (!this.observerPrefix) {
|
||||
if (!this.observerPrefix || !response.headers) {
|
||||
return;
|
||||
}
|
||||
let headerVal = response.headers[headerName];
|
||||
@ -243,8 +280,8 @@ this.HawkClient.prototype = {
|
||||
try {
|
||||
backoffInterval = parseInt(headerVal, 10);
|
||||
} catch (ex) {
|
||||
this._log.error("hawkclient response had invalid backoff value in '" +
|
||||
headerName + "' header: " + headerVal);
|
||||
log.error("hawkclient response had invalid backoff value in '" +
|
||||
headerName + "' header: " + headerVal);
|
||||
return;
|
||||
}
|
||||
Observers.notify(this.observerPrefix + ":backoff:interval", backoffInterval);
|
||||
@ -254,4 +291,5 @@ this.HawkClient.prototype = {
|
||||
newHAWKAuthenticatedRESTRequest: function(uri, credentials, extra) {
|
||||
return new HAWKAuthenticatedRESTRequest(uri, credentials, extra);
|
||||
},
|
||||
|
||||
}
|
||||
|
158
services/mobileid/MobileIdentityClient.jsm
Normal file
158
services/mobileid/MobileIdentityClient.jsm
Normal file
@ -0,0 +1,158 @@
|
||||
/* 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/. */
|
||||
|
||||
// REST client for
|
||||
// https://github.com/mozilla-services/msisdn-gateway/blob/master/API.md
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["MobileIdentityClient"];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://services-common/hawkclient.js");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
this.MobileIdentityClient = function(aServerUrl) {
|
||||
let serverUrl = aServerUrl || SERVER_URL;
|
||||
let forceHttps = false;
|
||||
try {
|
||||
// TODO: Force https in production. Bug 1021595.
|
||||
forceHttps = Services.prefs.getBoolPref(PREF_FORCE_HTTPS);
|
||||
} catch(e) {
|
||||
log.warn("Getting force HTTPS pref failed. If this was not intentional " +
|
||||
"check that " + PREF_FORCE_HTTPS + " is defined");
|
||||
}
|
||||
|
||||
log.debug("Force HTTPS " + forceHttps);
|
||||
|
||||
if (forceHttps && !/^https/.exec(serverUrl.toLowerCase())) {
|
||||
throw new Error(ERROR_INTERNAL_HTTP_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
this.hawk = new HawkClient(SERVER_URL);
|
||||
this.hawk.observerPrefix = "MobileId:hawk";
|
||||
};
|
||||
|
||||
this.MobileIdentityClient.prototype = {
|
||||
|
||||
discover: function(aMsisdn, aMcc, aMnc, aRoaming) {
|
||||
return this._request(DISCOVER, "POST", null, {
|
||||
msisdn: aMsisdn || undefined,
|
||||
mcc: aMcc,
|
||||
mnc: aMnc,
|
||||
roaming: aRoaming
|
||||
});
|
||||
},
|
||||
|
||||
register: function() {
|
||||
return this._request(REGISTER, "POST", null, {});
|
||||
},
|
||||
|
||||
smsMtVerify: function(aSessionToken, aMsisdn, aWantShortCode = false) {
|
||||
let credentials = this._deriveHawkCredentials(aSessionToken);
|
||||
return this._request(SMS_MT_VERIFY, "POST", credentials, {
|
||||
msisdn: aMsisdn,
|
||||
shortVerificationCode: aWantShortCode
|
||||
});
|
||||
},
|
||||
|
||||
verifyCode: function(aSessionToken, aVerificationCode) {
|
||||
log.debug("verificationCode " + aVerificationCode);
|
||||
let credentials = this._deriveHawkCredentials(aSessionToken);
|
||||
return this._request(SMS_VERIFY_CODE, "POST", credentials, {
|
||||
code: aVerificationCode
|
||||
});
|
||||
},
|
||||
|
||||
sign: function(aSessionToken, aDuration, aPublicKey) {
|
||||
let credentials = this._deriveHawkCredentials(aSessionToken);
|
||||
return this._request(SIGN, "POST", credentials, {
|
||||
duration: aDuration,
|
||||
publicKey: aPublicKey
|
||||
});
|
||||
},
|
||||
|
||||
unregister: function(aSessionToken) {
|
||||
let credentials = this._deriveHawkCredentials(aSessionToken);
|
||||
return this._request(UNREGISTER, "POST", credentials, {});
|
||||
},
|
||||
|
||||
/**
|
||||
* The MobileID server expects requests to certain endpoints to be
|
||||
* authorized using Hawk.
|
||||
*
|
||||
* Hawk credentials are derived using shared secrets.
|
||||
*
|
||||
* @param tokenHex
|
||||
* The current session token encoded in hex
|
||||
* @param context
|
||||
* A context for the credentials
|
||||
* @param size
|
||||
* The size in bytes of the expected derived buffer
|
||||
* @return credentials
|
||||
* Returns an object:
|
||||
* {
|
||||
* algorithm: sha256
|
||||
* id: the Hawk id (from the first 32 bytes derived)
|
||||
* key: the Hawk key (from bytes 32 to 64)
|
||||
* }
|
||||
*/
|
||||
_deriveHawkCredentials: function(aSessionToken) {
|
||||
let token = CommonUtils.hexToBytes(aSessionToken);
|
||||
let out = CryptoUtils.hkdf(token, undefined,
|
||||
CREDENTIALS_DERIVATION_INFO,
|
||||
CREDENTIALS_DERIVATION_SIZE);
|
||||
return {
|
||||
algorithm: "sha256",
|
||||
key: CommonUtils.bytesAsHex(out.slice(32, 64)),
|
||||
id: CommonUtils.bytesAsHex(out.slice(0, 32))
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* A general method for sending raw API calls to the mobile id verification
|
||||
* server.
|
||||
* All request bodies and responses are JSON.
|
||||
*
|
||||
* @param path
|
||||
* API endpoint path
|
||||
* @param method
|
||||
* The HTTP request method
|
||||
* @param credentials
|
||||
* Hawk credentials
|
||||
* @param jsonPayload
|
||||
* A JSON payload
|
||||
* @return Promise
|
||||
* Returns a promise that resolves to the JSON response of the API
|
||||
* call, or is rejected with an error.
|
||||
*/
|
||||
_request: function(path, method, credentials, jsonPayload) {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
this.hawk.request(path, method, credentials, jsonPayload).then(
|
||||
(responseText) => {
|
||||
log.debug("MobileIdentityClient -> responseText " + responseText);
|
||||
try {
|
||||
let response = JSON.parse(responseText);
|
||||
deferred.resolve(response);
|
||||
} catch (err) {
|
||||
deferred.reject({error: err});
|
||||
}
|
||||
},
|
||||
|
||||
(error) => {
|
||||
log.error("MobileIdentityClient -> Error ${}", error);
|
||||
deferred.reject(SERVER_ERRNO_TO_ERROR[error.errno] || ERROR_UNKNOWN);
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
};
|
143
services/mobileid/MobileIdentityCommon.jsm
Normal file
143
services/mobileid/MobileIdentityCommon.jsm
Normal file
@ -0,0 +1,143 @@
|
||||
/* 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/. */
|
||||
|
||||
const { interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
|
||||
// loglevel should be one of "Fatal", "Error", "Warn", "Info", "Config",
|
||||
// "Debug", "Trace" or "All". If none is specified, "Error" will be used by
|
||||
// default.
|
||||
const PREF_LOG_LEVEL = "services.mobileid.loglevel";
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "log", function() {
|
||||
let log = Log.repository.getLogger("MobileId");
|
||||
log.addAppender(new Log.DumpAppender());
|
||||
log.level = Log.Level.Error;
|
||||
try {
|
||||
let level =
|
||||
Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING
|
||||
&& Services.prefs.getCharPref(PREF_LOG_LEVEL);
|
||||
log.level = Log.Level[level] || Log.Level.Error;
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
}
|
||||
|
||||
return log;
|
||||
});
|
||||
|
||||
this.PREF_FORCE_HTTPS = "services.mobileid.forcehttps";
|
||||
|
||||
// Permission.
|
||||
this.MOBILEID_PERM = "mobileid";
|
||||
|
||||
// IPC messages.
|
||||
this.GET_ASSERTION_IPC_MSG = "MobileId:GetAssertion";
|
||||
|
||||
// Verification methods.
|
||||
this.SMS_MT = "sms/mt";
|
||||
this.SMS_MO_MT = "sms/momt";
|
||||
|
||||
// Server endpoints.
|
||||
this.DISCOVER = "/discover";
|
||||
this.REGISTER = "/register";
|
||||
this.SMS_MT_VERIFY = "/" + this.SMS_MT + "/verify";
|
||||
this.SMS_MO_MT_VERIFY = "/" + this.SMS_MO_MT + "/verify";
|
||||
this.SMS_VERIFY_CODE = "/sms/verify_code";
|
||||
this.SIGN = "/certificate/sign";
|
||||
this.UNREGISTER = "/unregister";
|
||||
|
||||
// Server consts.
|
||||
this.SERVER_URL = Services.prefs.getCharPref("services.mobileid.server.uri");
|
||||
this.CREDENTIALS_DERIVATION_INFO = "identity.mozilla.com/picl/v1/sessionToken";
|
||||
this.CREDENTIALS_DERIVATION_SIZE = 2 * 32;
|
||||
|
||||
this.SILENT_SMS_RECEIVED_TOPIC = "silent-sms-received";
|
||||
|
||||
this.ASSERTION_LIFETIME = 1000 * 60 * 5; // 5 minutes.
|
||||
this.CERTIFICATE_LIFETIME = 1000 * 3600 * 6; // 6 hours.
|
||||
this.KEY_LIFETIME = 1000 * 3600 * 12; // 12 hours.
|
||||
|
||||
this.VERIFICATIONCODE_TIMEOUT = 60000;
|
||||
this.VERIFICATIONCODE_RETRIES = 3;
|
||||
|
||||
// Internal Errors.
|
||||
this.ERROR_INTERNAL_CANNOT_CREATE_VERIFICATION_FLOW = "INTERNAL_CANNOT_CREATE_VERIFICATION_FLOW";
|
||||
this.ERROR_INTERNAL_CANNOT_GENERATE_ASSERTION = "INTERNAL_CANNOT_GENERATE_ASSERTION";
|
||||
this.ERROR_INTERNAL_CANNOT_VERIFY_SELECTION = "INTERNAL_CANNOT_VERIFY_SELECTION";
|
||||
this.ERROR_INTERNAL_DB_ERROR = "INTERNAL_DB_ERROR";
|
||||
this.ERROR_INTERNAL_HTTP_NOT_ALLOWED = "INTERNAL_HTTP_NOT_ALLOWED";
|
||||
this.ERROR_INTERNAL_INVALID_CERTIFICATE = "INTERNAL_INVALID_CERTIFICATE";
|
||||
this.ERROR_INTERNAL_INVALID_PROMPT_RESULT = "INTERNAL_INVALID_PROMPT_RESULT";
|
||||
this.ERROR_INTERNAL_INVALID_USER_SELECTION = "INTERNAL_INVALID_USER_SELECTION";
|
||||
this.ERROR_INTERNAL_INVALID_VERIFICATION_FLOW = "INTERNAL_INVALID_VERIFICATION_FLOW";
|
||||
this.ERROR_INTERNAL_INVALID_VERIFICATION_RESULT = "INTERNAL_INVALID_VERIFICATION_RESULT";
|
||||
this.ERROR_INTERNAL_UNEXPECTED = "INTERNAL_UNEXPECTED";
|
||||
|
||||
// Errors.
|
||||
this.ERROR_ENDPOINT_NOT_SUPPORTED = "ENDPOINT_NOT_SUPPORTED";
|
||||
this.ERROR_INVALID_ASSERTION = "INVALID_ASSERTION";
|
||||
this.ERROR_INVALID_AUTH_TOKEN = "INVALID_AUTH_TOKEN";
|
||||
this.ERROR_INVALID_BODY_JSON = "INVALID_BODY_JSON";
|
||||
this.ERROR_INVALID_BODY_MISSING_PARAMS = "INVALID_BODY_MISSING_PARAMS";
|
||||
this.ERROR_INVALID_BODY_PARAMS = "INVALID_BODY_PARAMS";
|
||||
this.ERROR_INVALID_PHONE_NUMBER = "INVALID_PHONE_NUMBER";
|
||||
this.ERROR_INVALID_PROMPT_RESULT = "INVALID_PROMPT_RESULT";
|
||||
this.ERROR_INVALID_REQUEST_SIGNATURE = "INVALID_REQUEST_SIGNATURE";
|
||||
this.ERROR_INVALID_VERIFICATION_CODE = "INVALID_VERIFICATION_CODE";
|
||||
this.ERROR_MISSING_CONTENT_LENGTH_HEADER = "MISSING_CONTENT_LENGTH_HEADER";
|
||||
this.ERROR_NO_RETRIES_LEFT = "NO_RETRIES_LEFT";
|
||||
this.ERROR_OFFLINE = "OFFLINE";
|
||||
this.ERROR_REQUEST_BODY_TOO_LARGE = "REQUEST_BODY_TOO_LARGE";
|
||||
this.ERROR_SERVICE_TEMPORARILY_UNAVAILABLE = "SERVICE_TEMPORARILY_UNAVAILABLE";
|
||||
this.ERROR_TOO_MANY_REQUESTS_MSISDN = "TOO_MANY_REQUESTS_MSISDN";
|
||||
this.ERROR_TOO_MANY_REQUESTS_UNSPECIFIED = "TOO_MANY_REQUESTS_UNSPECIFIED";
|
||||
this.ERROR_TOO_MANY_REQUESTS_VERIFICAITON_CODE = "TOO_MANY_REQUESTS_VERIFICATION_CODE";
|
||||
this.ERROR_TOO_MANY_REQUESTS_VERIFICATION_METHOD = "TOO_MANY_REQUESTS_VERIFICATION_METHOD";
|
||||
this.ERROR_UNKNOWN = "UNKNOWN";
|
||||
this.ERROR_UNVERIFIED_ACCOUNT = "UNVERIFIED_ACCOUNT";
|
||||
this.ERROR_VERIFICATION_CODE_TIMEOUT = "VERIFICATION_CODE_TIMEOUT";
|
||||
|
||||
// Server errno.
|
||||
// From https://github.com/mozilla-services/msisdn-gateway/blob/master/API.md#response-format
|
||||
this.ERRNO_UNVERIFIED_ACCOUNT = 104;
|
||||
this.ERRNO_INVALID_VERIFICATION_CODE = 105;
|
||||
this.ERRNO_INVALID_BODY_JSON = 106;
|
||||
this.ERRNO_INVALID_BODY_INVALID_PARAMS = 107;
|
||||
this.ERRNO_INVALID_BODY_MISSING_PARAMS = 108;
|
||||
this.ERRNO_INVALID_REQUEST_SIGNATURE = 109;
|
||||
this.ERRNO_INVALID_AUTH_TOKEN = 110;
|
||||
this.ERRNO_ENDPOINT_NOT_SUPPORTED = 111;
|
||||
this.ERRNO_MISSING_CONTENT_LENGTH_HEADER = 112;
|
||||
this.ERRNO_REQUEST_BODY_TOO_LARGE = 113;
|
||||
this.ERRNO_TOO_MANY_REQUESTS_VERIFICATION_CODE = 114;
|
||||
this.ERRNO_TOO_MANY_REQUESTS_MSISDN = 115;
|
||||
this.ERRNO_TOO_MANY_REQUESTS_VERIFICATION_METHOD = 116;
|
||||
this.ERRNO_TOO_MANY_REQUESTS_UNSPECIFIED = 117;
|
||||
this.ERRNO_SERVICE_TEMPORARILY_UNAVAILABLE = 201;
|
||||
this.ERRNO_UNKNOWN_ERROR = 999;
|
||||
|
||||
// Error matching.
|
||||
this.SERVER_ERRNO_TO_ERROR = {};
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_UNVERIFIED_ACCOUNT] = ERROR_UNVERIFIED_ACCOUNT;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_VERIFICATION_CODE] = ERROR_INVALID_VERIFICATION_CODE;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_BODY_JSON] = ERROR_INVALID_BODY_JSON;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_BODY_INVALID_PARAMS] = ERROR_INVALID_BODY_PARAMS;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_BODY_MISSING_PARAMS] = ERROR_INVALID_BODY_MISSING_PARAMS;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_REQUEST_SIGNATURE] = ERROR_INVALID_REQUEST_SIGNATURE;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_INVALID_AUTH_TOKEN] = ERROR_INVALID_AUTH_TOKEN;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_ENDPOINT_NOT_SUPPORTED] = ERROR_ENDPOINT_NOT_SUPPORTED;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_MISSING_CONTENT_LENGTH_HEADER] = ERROR_MISSING_CONTENT_LENGTH_HEADER;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_REQUEST_BODY_TOO_LARGE] = ERROR_REQUEST_BODY_TOO_LARGE;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_TOO_MANY_REQUESTS_VERIFICATION_CODE] = ERROR_TOO_MANY_REQUESTS_VERIFICAITON_CODE;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_TOO_MANY_REQUESTS_MSISDN] = ERROR_TOO_MANY_REQUESTS_MSISDN;;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_TOO_MANY_REQUESTS_VERIFICATION_METHOD] = ERROR_TOO_MANY_REQUESTS_VERIFICATION_METHOD;;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_TOO_MANY_REQUESTS_UNSPECIFIED] = ERROR_TOO_MANY_REQUESTS_UNSPECIFIED;;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_SERVICE_TEMPORARILY_UNAVAILABLE] = ERROR_SERVICE_TEMPORARILY_UNAVAILABLE;
|
||||
SERVER_ERRNO_TO_ERROR[ERRNO_UNKNOWN_ERROR] = ERROR_UNKNOWN;
|
||||
|
||||
// Allow this file to be imported via Components.utils.import().
|
||||
this.EXPORTED_SYMBOLS = Object.keys(this);
|
173
services/mobileid/MobileIdentityCredentialsStore.jsm
Normal file
173
services/mobileid/MobileIdentityCredentialsStore.jsm
Normal file
@ -0,0 +1,173 @@
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["MobileIdentityCredentialsStore"];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
|
||||
Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
|
||||
const CREDENTIALS_DB_NAME = "mobile-id-credentials";
|
||||
const CREDENTIALS_DB_VERSION = 1;
|
||||
const CREDENTIALS_STORE_NAME = "credentials-store";
|
||||
|
||||
this.MobileIdentityCredentialsStore = function() {
|
||||
};
|
||||
|
||||
this.MobileIdentityCredentialsStore.prototype = {
|
||||
|
||||
__proto__: IndexedDBHelper.prototype,
|
||||
|
||||
init: function() {
|
||||
log.debug("MobileIdentityCredentialsStore init");
|
||||
this.initDBHelper(CREDENTIALS_DB_NAME,
|
||||
CREDENTIALS_DB_VERSION,
|
||||
[CREDENTIALS_STORE_NAME]);
|
||||
},
|
||||
|
||||
upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) {
|
||||
log.debug("upgradeSchema");
|
||||
/**
|
||||
* We will be storing objects like:
|
||||
* {
|
||||
* msisdn: <string> (key),
|
||||
* iccId: <string> (index),
|
||||
* origin: <array> (index),
|
||||
* msisdnSessionToken: <string>
|
||||
* }
|
||||
*/
|
||||
let objectStore = aDb.createObjectStore(CREDENTIALS_STORE_NAME, {
|
||||
keyPath: "msisdn"
|
||||
});
|
||||
|
||||
objectStore.createIndex("iccId", "iccId", { unique: true });
|
||||
objectStore.createIndex("origin", "origin", { unique: true, multiEntry: true });
|
||||
},
|
||||
|
||||
add: function(aIccId, aMsisdn, aOrigin, aSessionToken) {
|
||||
log.debug("put " + aIccId + ", " + aMsisdn + ", " + aOrigin + ", " +
|
||||
aSessionToken);
|
||||
if (!aOrigin || !aSessionToken) {
|
||||
return Promise.reject(ERROR_INTERNAL_DB_ERROR);
|
||||
}
|
||||
|
||||
let deferred = Promise.defer();
|
||||
|
||||
// We first try get an existing record for the given MSISDN.
|
||||
this.newTxn(
|
||||
"readwrite",
|
||||
CREDENTIALS_STORE_NAME,
|
||||
(aTxn, aStore) => {
|
||||
let range = IDBKeyRange.only(aMsisdn);
|
||||
let cursorReq = aStore.openCursor(range);
|
||||
cursorReq.onsuccess = function(aEvent) {
|
||||
let cursor = aEvent.target.result;
|
||||
let record;
|
||||
// If we already have a record of this MSISDN, we add the origin to
|
||||
// the list of allowed origins.
|
||||
if (cursor && cursor.value) {
|
||||
record = cursor.value;
|
||||
if (record.origin.indexOf(aOrigin) == -1) {
|
||||
record.origin.push(aOrigin);
|
||||
}
|
||||
cursor.update(record);
|
||||
} else {
|
||||
// Otherwise, we store a new record.
|
||||
record = {
|
||||
iccId: aIccId,
|
||||
msisdn: aMsisdn,
|
||||
origin: [aOrigin],
|
||||
sessionToken: aSessionToken
|
||||
};
|
||||
aStore.add(record);
|
||||
}
|
||||
deferred.resolve();
|
||||
};
|
||||
cursorReq.onerror = function(aEvent) {
|
||||
log.error(aEvent.target.error);
|
||||
deferred.reject(ERROR_INTERNAL_DB_ERROR);
|
||||
};
|
||||
}, null, deferred.reject);
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
getByMsisdn: function(aMsisdn) {
|
||||
log.debug("getByMsisdn " + aMsisdn);
|
||||
if (!aMsisdn) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
let deferred = Promise.defer();
|
||||
this.newTxn(
|
||||
"readonly",
|
||||
CREDENTIALS_STORE_NAME,
|
||||
(aTxn, aStore) => {
|
||||
aStore.get(aMsisdn).onsuccess = function(aEvent) {
|
||||
aTxn.result = aEvent.target.result;
|
||||
};
|
||||
},
|
||||
function(result) {
|
||||
deferred.resolve(result);
|
||||
},
|
||||
deferred.reject
|
||||
);
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
getByIndex: function(aIndex, aValue) {
|
||||
log.debug("getByIndex " + aIndex + ", " + aValue);
|
||||
if (!aValue || !aIndex) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
let deferred = Promise.defer();
|
||||
this.newTxn(
|
||||
"readonly",
|
||||
CREDENTIALS_STORE_NAME,
|
||||
(aTxn, aStore) => {
|
||||
let index = aStore.index(aIndex);
|
||||
index.get(aValue).onsuccess = function(aEvent) {
|
||||
aTxn.result = aEvent.target.result;
|
||||
};
|
||||
},
|
||||
function(result) {
|
||||
deferred.resolve(result);
|
||||
},
|
||||
deferred.reject
|
||||
);
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
getByOrigin: function(aOrigin) {
|
||||
return this.getByIndex("origin", aOrigin);
|
||||
},
|
||||
|
||||
getByIccId: function(aIccId) {
|
||||
return this.getByIndex("iccId", aIccId);
|
||||
},
|
||||
|
||||
delete: function(aMsisdn) {
|
||||
log.debug("delete " + aMsisdn);
|
||||
if (!aMsisdn) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let deferred = Promise.defer();
|
||||
this.newTxn(
|
||||
"readwrite",
|
||||
CREDENTIALS_STORE_NAME,
|
||||
(aTxn, aStore) => {
|
||||
aStore.delete(aMsisdn);
|
||||
},
|
||||
deferred.resolve,
|
||||
deferred.reject
|
||||
);
|
||||
return deferred.promise;
|
||||
}
|
||||
};
|
824
services/mobileid/MobileIdentityManager.jsm
Normal file
824
services/mobileid/MobileIdentityManager.jsm
Normal file
@ -0,0 +1,824 @@
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = [];
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
|
||||
Cu.import("resource://gre/modules/MobileIdentityUIGlueCommon.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MobileIdentityCredentialsStore",
|
||||
"resource://gre/modules/MobileIdentityCredentialsStore.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MobileIdentityClient",
|
||||
"resource://gre/modules/MobileIdentityClient.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MobileIdentitySmsMtVerificationFlow",
|
||||
"resource://gre/modules/MobileIdentitySmsMtVerificationFlow.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MobileIdentitySmsMoMtVerificationFlow",
|
||||
"resource://gre/modules/MobileIdentitySmsMoMtVerificationFlow.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PhoneNumberUtils",
|
||||
"resource://gre/modules/PhoneNumberUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto",
|
||||
"resource://gre/modules/identity/jwcrypto.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
|
||||
"@mozilla.org/uuid-generator;1",
|
||||
"nsIUUIDGenerator");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
|
||||
"@mozilla.org/parentprocessmessagemanager;1",
|
||||
"nsIMessageListenerManager");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "permissionManager",
|
||||
"@mozilla.org/permissionmanager;1",
|
||||
"nsIPermissionManager");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "securityManager",
|
||||
"@mozilla.org/scriptsecuritymanager;1",
|
||||
"nsIScriptSecurityManager");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "appsService",
|
||||
"@mozilla.org/AppsService;1",
|
||||
"nsIAppsService");
|
||||
|
||||
#ifdef MOZ_B2G_RIL
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "gRil",
|
||||
"@mozilla.org/ril;1",
|
||||
"nsIRadioInterfaceLayer");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "iccProvider",
|
||||
"@mozilla.org/ril/content-helper;1",
|
||||
"nsIIccProvider");
|
||||
#endif
|
||||
|
||||
|
||||
let MobileIdentityManager = {
|
||||
|
||||
init: function() {
|
||||
log.debug("MobileIdentityManager init");
|
||||
Services.obs.addObserver(this, "xpcom-shutdown", false);
|
||||
ppmm.addMessageListener(GET_ASSERTION_IPC_MSG, this);
|
||||
this.messageManagers = {};
|
||||
// TODO: Store keyPairs and certificates in disk. Bug 1021605.
|
||||
this.keyPairs = {};
|
||||
this.certificates = {};
|
||||
},
|
||||
|
||||
receiveMessage: function(aMessage) {
|
||||
log.debug("Received " + aMessage.name);
|
||||
|
||||
if (aMessage.name !== GET_ASSERTION_IPC_MSG) {
|
||||
return;
|
||||
}
|
||||
|
||||
let msg = aMessage.json;
|
||||
|
||||
// We save the message target message manager so we can later dispatch
|
||||
// back messages without broadcasting to all child processes.
|
||||
let promiseId = msg.promiseId;
|
||||
this.messageManagers[promiseId] = aMessage.target;
|
||||
|
||||
this.getMobileIdAssertion(aMessage.principal, promiseId,
|
||||
msg.msisdn, msg.prompt);
|
||||
},
|
||||
|
||||
observe: function(subject, topic, data) {
|
||||
if (topic != "xpcom-shutdown") {
|
||||
return;
|
||||
}
|
||||
|
||||
ppmm.removeMessageListener(GET_ASSERTION_IPC_MSG, this);
|
||||
Services.obs.removeObserver(this, "xpcom-shutdown");
|
||||
this.messageManagers = null;
|
||||
},
|
||||
|
||||
|
||||
/*********************************************************
|
||||
* Getters
|
||||
********************************************************/
|
||||
|
||||
get iccInfo() {
|
||||
#ifdef MOZ_B2G_RIL
|
||||
if (this._iccInfo) {
|
||||
return this._iccInfo;
|
||||
}
|
||||
|
||||
this._iccInfo = [];
|
||||
for (let i = 0; i < gRil.numRadioInterfaces; i++) {
|
||||
let rilContext = gRil.getRadioInterface(i).rilContext;
|
||||
if (!rilContext) {
|
||||
log.warn("Tried to get the RIL context for an invalid service ID " + i);
|
||||
continue;
|
||||
}
|
||||
let info = rilContext.iccInfo;
|
||||
if (!info) {
|
||||
log.warn("No ICC info");
|
||||
continue;
|
||||
}
|
||||
|
||||
let operator = null;
|
||||
if (rilContext.voice.network &&
|
||||
rilContext.voice.network.shortName &&
|
||||
rilContext.voice.network.shortName.length) {
|
||||
operator = rilContext.voice.network.shortName;
|
||||
} else if (rilContext.data.network &&
|
||||
rilContext.data.network.shortName &&
|
||||
rilContext.data.network.shortName.length) {
|
||||
operator = rilContext.data.network.shortName;
|
||||
}
|
||||
|
||||
this._iccInfo.push({
|
||||
iccId: info.iccid,
|
||||
mcc: info.mcc,
|
||||
mnc: info.mnc,
|
||||
// GSM SIMs may have MSISDN while CDMA SIMs may have MDN
|
||||
msisdn: info.msisdn || info.mdn || null,
|
||||
operator: operator,
|
||||
serviceId: i,
|
||||
roaming: rilContext.voice.roaming
|
||||
});
|
||||
}
|
||||
|
||||
return this._iccInfo;
|
||||
#endif
|
||||
return null;
|
||||
},
|
||||
|
||||
get credStore() {
|
||||
if (!this._credStore) {
|
||||
this._credStore = new MobileIdentityCredentialsStore();
|
||||
this._credStore.init();
|
||||
}
|
||||
return this._credStore;
|
||||
},
|
||||
|
||||
get ui() {
|
||||
if (!this._ui) {
|
||||
this._ui = Cc["@mozilla.org/services/mobileid-ui-glue;1"]
|
||||
.createInstance(Ci.nsIMobileIdentityUIGlue);
|
||||
this._ui.oncancel = this.onUICancel.bind(this);
|
||||
this._ui.onresendcode = this.onUIResendCode.bind(this);
|
||||
}
|
||||
return this._ui;
|
||||
},
|
||||
|
||||
get client() {
|
||||
if (!this._client) {
|
||||
this._client = new MobileIdentityClient();
|
||||
}
|
||||
return this._client;
|
||||
},
|
||||
|
||||
get isMultiSim() {
|
||||
return this.iccInfo && this.iccInfo.length > 1;
|
||||
},
|
||||
|
||||
getVerificationOptionsForIcc: function(aServiceId) {
|
||||
log.debug("getVerificationOptionsForIcc " + aServiceId);
|
||||
log.debug("iccInfo ${}", this.iccInfo[aServiceId]);
|
||||
// First of all we need to check if we already have existing credentials
|
||||
// for the given SIM information (ICC id or MSISDN). If we have no valid
|
||||
// credentials, we have to check with the server which options to do we
|
||||
// have to verify the associated phone number.
|
||||
return this.credStore.getByIccId(this.iccInfo[aServiceId].iccId)
|
||||
.then(
|
||||
(creds) => {
|
||||
if (creds) {
|
||||
this.iccInfo[aServiceId].credentials = creds;
|
||||
return;
|
||||
}
|
||||
return this.credStore.getByMsisdn(this.iccInfo[aServiceId].msisdn);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
(creds) => {
|
||||
if (creds) {
|
||||
this.iccInfo[aServiceId].credentials = creds;
|
||||
return;
|
||||
}
|
||||
// We have no credentials for this SIM, so we need to ask the server
|
||||
// which options do we have to verify the phone number.
|
||||
// But we need to be online...
|
||||
if (Services.io.offline) {
|
||||
return Promise.reject(ERROR_OFFLINE);
|
||||
}
|
||||
return this.client.discover(this.iccInfo[aServiceId].msisdn,
|
||||
this.iccInfo[aServiceId].mcc,
|
||||
this.iccInfo[aServiceId].mnc,
|
||||
this.iccInfo[aServiceId].roaming);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
(result) => {
|
||||
log.debug("Discover result ${}", result);
|
||||
if (!result || !result.verificationMethods) {
|
||||
return;
|
||||
}
|
||||
this.iccInfo[aServiceId].verificationMethods = result.verificationMethods;
|
||||
this.iccInfo[aServiceId].verificationDetails = result.verificationDetails;
|
||||
this.iccInfo[aServiceId].canDoSilentVerification =
|
||||
(result.verificationMethods.indexOf(SMS_MO_MT) != -1);
|
||||
return;
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
getVerificationOptions: function() {
|
||||
log.debug("getVerificationOptions");
|
||||
// We try to get if we already have credentials for any of the inserted
|
||||
// SIM cards if any is available and we try to get the possible
|
||||
// verification mechanisms for these SIM cards.
|
||||
// All this information will be stored in iccInfo.
|
||||
if (!this.iccInfo || !this.iccInfo.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let promises = [];
|
||||
for (let i = 0; i < this.iccInfo.length; i++) {
|
||||
promises.push(this.getVerificationOptionsForIcc(i));
|
||||
}
|
||||
return Promise.all(promises);
|
||||
},
|
||||
|
||||
getKeyPair: function(aSessionToken) {
|
||||
if (this.keyPairs[aSessionToken] &&
|
||||
this.keyPairs[aSessionToken].validUntil > this.client.hawk.now()) {
|
||||
return Promise.resolve(this.keyPairs[aSessionToken].keyPair);
|
||||
}
|
||||
|
||||
let validUntil = this.client.hawk.now() + KEY_LIFETIME;
|
||||
let deferred = Promise.defer();
|
||||
jwcrypto.generateKeyPair("DS160", (error, kp) => {
|
||||
if (error) {
|
||||
return deferred.reject(error);
|
||||
}
|
||||
this.keyPairs[aSessionToken] = {
|
||||
keyPair: kp,
|
||||
validUntil: validUntil
|
||||
};
|
||||
delete this.certificates[aSessionToken];
|
||||
deferred.resolve(kp);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
getCertificate: function(aSessionToken, aPublicKey) {
|
||||
if (this.certificates[aSessionToken] &&
|
||||
this.certificates[aSessionToken].validUntil > this.client.hawk.now()) {
|
||||
return Promise.resolve(this.certificates[aSessionToken].cert);
|
||||
}
|
||||
|
||||
if (Services.io.offline) {
|
||||
return Promise.reject(ERROR_OFFLINE);
|
||||
}
|
||||
|
||||
let validUntil = this.client.hawk.now() + KEY_LIFETIME;
|
||||
let deferred = Promise.defer();
|
||||
this.client.sign(aSessionToken, CERTIFICATE_LIFETIME,
|
||||
aPublicKey)
|
||||
.then(
|
||||
(signedCert) => {
|
||||
this.certificates[aSessionToken] = {
|
||||
cert: signedCert.cert,
|
||||
validUntil: validUntil
|
||||
};
|
||||
deferred.resolve(signedCert.cert);
|
||||
},
|
||||
deferred.reject
|
||||
);
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/*********************************************************
|
||||
* UI callbacks
|
||||
********************************************************/
|
||||
|
||||
onUICancel: function() {
|
||||
log.debug("UI cancel");
|
||||
if (this.activeVerificationFlow) {
|
||||
this.activeVerificationFlow.cleanup(true);
|
||||
}
|
||||
},
|
||||
|
||||
onUIResendCode: function() {
|
||||
log.debug("UI resend code");
|
||||
if (!this.activeVerificationFlow) {
|
||||
return;
|
||||
}
|
||||
this.doVerification();
|
||||
},
|
||||
|
||||
/*********************************************************
|
||||
* Permissions helpers
|
||||
********************************************************/
|
||||
|
||||
hasPermission: function(aPrincipal) {
|
||||
let permission = permissionManager.testPermissionFromPrincipal(aPrincipal,
|
||||
MOBILEID_PERM);
|
||||
return permission == Ci.nsIPermissionManager.ALLOW_ACTION;
|
||||
},
|
||||
|
||||
addPermission: function(aPrincipal) {
|
||||
permissionManager.addFromPrincipal(aPrincipal, MOBILEID_PERM,
|
||||
Ci.nsIPermissionManager.ALLOW_ACTION);
|
||||
},
|
||||
|
||||
/*********************************************************
|
||||
* Phone number verification
|
||||
********************************************************/
|
||||
|
||||
rejectVerification: function(aReason) {
|
||||
if (!this.activeVerificationDeferred) {
|
||||
return;
|
||||
}
|
||||
this.activeVerificationDeferred.reject(aReason);
|
||||
this.activeVerificationDeferred = null;
|
||||
this.cleanupVerification(true);
|
||||
},
|
||||
|
||||
resolveVerification: function(aResult) {
|
||||
if (!this.activeVerificationDeferred) {
|
||||
return;
|
||||
}
|
||||
this.activeVerificationDeferred.resolve(aResult);
|
||||
this.activeVerificationDeferred = null;
|
||||
this.cleanupVerification();
|
||||
},
|
||||
|
||||
cleanupVerification: function() {
|
||||
if (!this.activeVerificationFlow) {
|
||||
return;
|
||||
}
|
||||
this.activeVerificationFlow.cleanup();
|
||||
this.activeVerificationFlow = null;
|
||||
},
|
||||
|
||||
doVerification: function() {
|
||||
this.activeVerificationFlow.doVerification()
|
||||
.then(
|
||||
(verificationResult) => {
|
||||
log.debug("onVerificationResult ");
|
||||
if (!verificationResult || !verificationResult.sessionToken ||
|
||||
!verificationResult.msisdn) {
|
||||
return this.rejectVerification(
|
||||
ERROR_INTERNAL_INVALID_VERIFICATION_RESULT
|
||||
);
|
||||
}
|
||||
this.resolveVerification(verificationResult);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
null,
|
||||
reason => {
|
||||
// Verification timeout.
|
||||
log.warn("doVerification " + reason);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
_verificationFlow: function(aToVerify, aOrigin) {
|
||||
log.debug("toVerify ${}", aToVerify);
|
||||
|
||||
// We create the corresponding verification flow and save its instance
|
||||
// in case that we need to cancel it or retrigger it because the user
|
||||
// requested its cancelation or a resend of the verification code.
|
||||
if (aToVerify.verificationMethod.indexOf(SMS_MT) != -1 &&
|
||||
aToVerify.msisdn &&
|
||||
aToVerify.verificationDetails &&
|
||||
aToVerify.verificationDetails.mtSender) {
|
||||
this.activeVerificationFlow = new MobileIdentitySmsMtVerificationFlow(
|
||||
aOrigin,
|
||||
aToVerify.msisdn,
|
||||
aToVerify.iccId,
|
||||
aToVerify.serviceId === undefined, // external: the phone number does
|
||||
// not seem to belong to any of the
|
||||
// device SIM cards.
|
||||
aToVerify.verificationDetails.mtSender,
|
||||
this.ui,
|
||||
this.client
|
||||
);
|
||||
#ifdef MOZ_B2G_RIL
|
||||
} else if (aToVerify.verificationMethod.indexOf(SMS_MO_MT) != -1 &&
|
||||
aToVerify.serviceId &&
|
||||
aToVerify.verificationDetails &&
|
||||
aToVerify.verificationDetails.moVerifier &&
|
||||
aToVerify.verificationDetails.mtSender) {
|
||||
|
||||
this.activeVerificationFlow = new MobileIdentitySmsMoMtVerificationFlow(
|
||||
aOrigin,
|
||||
aToVerify.serviceId,
|
||||
aToVerify.iccId,
|
||||
aToVerify.verificationDetails.mtSender,
|
||||
aToVerify.verificationDetails.moVerifier,
|
||||
this.ui,
|
||||
this.client
|
||||
);
|
||||
#endif
|
||||
} else {
|
||||
return Promise.reject(ERROR_INTERNAL_CANNOT_VERIFY_SELECTION);
|
||||
}
|
||||
|
||||
if (!this.activeVerificationFlow) {
|
||||
return Promise.reject(ERROR_INTERNAL_CANNOT_CREATE_VERIFICATION_FLOW);
|
||||
}
|
||||
|
||||
this.activeVerificationDeferred = Promise.defer();
|
||||
this.doVerification();
|
||||
return this.activeVerificationDeferred.promise;
|
||||
},
|
||||
|
||||
verificationFlow: function(aUserSelection, aOrigin) {
|
||||
log.debug("verificationFlow ${}", aUserSelection);
|
||||
|
||||
if (!aUserSelection) {
|
||||
return Promise.reject(ERROR_INTERNAL_INVALID_USER_SELECTION);
|
||||
}
|
||||
|
||||
let serviceId = aUserSelection.serviceId || undefined;
|
||||
// We check if the user entered phone number corresponds with any of the
|
||||
// inserted SIMs known phone numbers.
|
||||
if (aUserSelection.msisdn && this.iccInfo) {
|
||||
for (let i = 0; i < this.iccInfo.length; i++) {
|
||||
if (aUserSelection.msisdn == this.iccInfo[i].msisdn) {
|
||||
serviceId = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let toVerify = {};
|
||||
|
||||
if (serviceId !== undefined) {
|
||||
log.debug("iccInfo ${}", this.iccInfo[serviceId]);
|
||||
toVerify.serviceId = serviceId;
|
||||
toVerify.iccId = this.iccInfo[serviceId].iccId;
|
||||
toVerify.msisdn = this.iccInfo[serviceId].msisdn;
|
||||
toVerify.verificationMethod =
|
||||
this.iccInfo[serviceId].verificationMethods[0];
|
||||
toVerify.verificationDetails =
|
||||
this.iccInfo[serviceId].verificationDetails[toVerify.verificationMethod];
|
||||
return this._verificationFlow(toVerify, aOrigin);
|
||||
} else {
|
||||
toVerify.msisdn = aUserSelection.msisdn;
|
||||
return this.client.discover(aUserSelection.msisdn,
|
||||
aUserSelection.mcc)
|
||||
.then(
|
||||
(discoverResult) => {
|
||||
if (!discoverResult || !discoverResult.verificationMethods) {
|
||||
return Promise.reject(ERROR_INTERNAL_UNEXPECTED);
|
||||
}
|
||||
log.debug("discoverResult ${}", discoverResult);
|
||||
toVerify.verificationMethod = discoverResult.verificationMethods[0];
|
||||
toVerify.verificationDetails =
|
||||
discoverResult.verificationDetails[toVerify.verificationMethod];
|
||||
return this._verificationFlow(toVerify, aOrigin);
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/*********************************************************
|
||||
* UI prompt functions.
|
||||
********************************************************/
|
||||
|
||||
// The phone number prompt will be used to confirm that the user wants to
|
||||
// verify and share a known phone number and to allow her to introduce an
|
||||
// external phone or to select between phone numbers or SIM cards (if the
|
||||
// phones are not known) in a multi-SIM scenario.
|
||||
// This prompt will be considered as the permission prompt and its choice
|
||||
// will be remembered per origin by default.
|
||||
prompt: function prompt(aPrincipal, aManifestURL, aPhoneInfo) {
|
||||
log.debug("prompt " + aPrincipal + ", " + aManifestURL + ", " +
|
||||
aPhoneInfo);
|
||||
|
||||
let phoneInfoArray = [];
|
||||
|
||||
if (aPhoneInfo) {
|
||||
phoneInfoArray.push(aPhoneInfo);
|
||||
}
|
||||
|
||||
if (this.iccInfo) {
|
||||
for (let i = 0; i < this.iccInfo.length; i++) {
|
||||
// If we don't know the msisdn, there is no previous credentials and
|
||||
// a silent verification is not possible, we don't allow the user to
|
||||
// choose this option.
|
||||
if (!this.iccInfo[i].msisdn && !this.iccInfo[i].credentials &&
|
||||
!this.iccInfo[i].canDoSilentVerification) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let phoneInfo = new MobileIdentityUIGluePhoneInfo(
|
||||
this.iccInfo[i].msisdn,
|
||||
this.iccInfo[i].operator,
|
||||
i, // service ID
|
||||
false, // external
|
||||
false // primary
|
||||
);
|
||||
phoneInfoArray.push(phoneInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return this.ui.startFlow(aManifestURL, phoneInfoArray)
|
||||
.then(
|
||||
(result) => {
|
||||
if (!result ||
|
||||
(!result.phoneNumber && !result.serviceId)) {
|
||||
return Promise.reject(ERROR_INTERNAL_INVALID_PROMPT_RESULT);
|
||||
}
|
||||
|
||||
let msisdn;
|
||||
let mcc;
|
||||
|
||||
// If the user selected one of the existing SIM cards we have to check
|
||||
// that we either have the MSISDN for that SIM or we can do a silent
|
||||
// verification that does not require us to have the MSISDN in advance.
|
||||
if (result.serviceId) {
|
||||
let icc = this.iccInfo[result.serviceId];
|
||||
log.debug("icc ${}", icc);
|
||||
if (!icc || !icc.msisdn && !icc.canDoSilentVerification) {
|
||||
return Promise.reject(ERROR_INTERNAL_CANNOT_VERIFY_SELECTION);
|
||||
}
|
||||
msisdn = icc.msisdn;
|
||||
mcc = icc.mcc;
|
||||
} else {
|
||||
msisdn = result.prefix ? result.prefix + result.phoneNumber
|
||||
: result.phoneNumber;
|
||||
mcc = result.mcc;
|
||||
}
|
||||
|
||||
// We need to check that the selected phone number is valid and
|
||||
// if it is not notify the UI about the error and allow the user to
|
||||
// retry.
|
||||
if (msisdn && mcc &&
|
||||
!PhoneNumberUtils.parseWithMCC(msisdn, mcc)) {
|
||||
this.ui.error(ERROR_INVALID_PHONE_NUMBER);
|
||||
return this.prompt(aPrincipal, aManifestURL, aPhoneInfo);
|
||||
}
|
||||
|
||||
log.debug("Selected msisdn (if any): " + msisdn + " - " + mcc);
|
||||
|
||||
// The user gave permission for the requester origin, so we store it.
|
||||
this.addPermission(aPrincipal);
|
||||
|
||||
return {
|
||||
msisdn: msisdn,
|
||||
mcc: mcc,
|
||||
serviceId: result.serviceId
|
||||
};
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
promptAndVerify: function(aPrincipal, aManifestURL, aCreds) {
|
||||
log.debug("promptAndVerify " + aPrincipal + ", " + aManifestURL +
|
||||
", ${}", aCreds);
|
||||
let userSelection;
|
||||
|
||||
if (Services.io.offline) {
|
||||
return Promise.reject(ERROR_OFFLINE);
|
||||
}
|
||||
|
||||
// Before prompting the user we need to check with the server the
|
||||
// phone number verification methods that are possible with the
|
||||
// SIMs inserted in the device.
|
||||
return this.getVerificationOptions()
|
||||
.then(
|
||||
() => {
|
||||
// If we have an exisiting credentials, we add its associated
|
||||
// phone number information to the list of choices to present
|
||||
// to the user within the selection prompt.
|
||||
let phoneInfo;
|
||||
if (aCreds) {
|
||||
phoneInfo = new MobileIdentityUIGluePhoneInfo(
|
||||
aCreds.msisdn,
|
||||
null, // operator
|
||||
null, // service ID
|
||||
!!aCreds.iccId, // external
|
||||
true // primary
|
||||
);
|
||||
}
|
||||
return this.prompt(aPrincipal, aManifestURL, phoneInfo);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
(promptResult) => {
|
||||
log.debug("promptResult ${}", promptResult);
|
||||
// If we had credentials and the user didn't change her
|
||||
// selection we return them. Otherwise, we need to verify
|
||||
// the new number.
|
||||
if (promptResult.msisdn && aCreds &&
|
||||
promptResult.msisdn == aCreds.msisdn) {
|
||||
return aCreds;
|
||||
}
|
||||
|
||||
// We might already have credentials for the user selected icc. In
|
||||
// that case, we update the credentials store with the new origin and
|
||||
// return the credentials.
|
||||
if (promptResult.serviceId) {
|
||||
let creds = this.iccInfo[promptResult.serviceId].credentials;
|
||||
if (creds) {
|
||||
this.credStore.add(creds.iccId, creds.msisdn, aPrincipal.origin,
|
||||
creds.sessionToken);
|
||||
return creds;
|
||||
}
|
||||
}
|
||||
|
||||
// Or we might already have credentials for the selected phone
|
||||
// number and so we do the same: update the credentials store with the
|
||||
// new origin and return the credentials.
|
||||
return this.credStore.getByMsisdn(promptResult.msisdn)
|
||||
.then(
|
||||
(creds) => {
|
||||
if (creds) {
|
||||
this.credStore.add(creds.iccId, creds.msisdn, aPrincipal.origin,
|
||||
creds.sessionToken);
|
||||
return creds;
|
||||
}
|
||||
// Otherwise, we need to verify the new number selected by the
|
||||
// user.
|
||||
return this.verificationFlow(promptResult, aPrincipal.origin);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/*********************************************************
|
||||
* Assertion generation
|
||||
********************************************************/
|
||||
|
||||
generateAssertion: function(aCredentials, aOrigin) {
|
||||
if (!aCredentials.sessionToken) {
|
||||
return Promise.reject(ERROR_INTERNAL_INVALID_TOKEN);
|
||||
}
|
||||
|
||||
let deferred = Promise.defer();
|
||||
|
||||
this.getKeyPair(aCredentials.sessionToken)
|
||||
.then(
|
||||
(keyPair) => {
|
||||
log.debug("keyPair " + keyPair.serializedPublicKey);
|
||||
let options = {
|
||||
duration: ASSERTION_LIFETIME,
|
||||
now: this.client.hawk.now(),
|
||||
localtimeOffsetMsec: this.client.hawk.localtimeOffsetMsec
|
||||
};
|
||||
|
||||
this.getCertificate(aCredentials.sessionToken,
|
||||
keyPair.serializedPublicKey)
|
||||
.then(
|
||||
(signedCert) => {
|
||||
log.debug("generateAssertion " + signedCert);
|
||||
jwcrypto.generateAssertion(signedCert, keyPair,
|
||||
aOrigin, options,
|
||||
(error, assertion) => {
|
||||
if (error) {
|
||||
log.error("Error generating assertion " + err);
|
||||
deferred.reject(error);
|
||||
return;
|
||||
}
|
||||
this.credStore.add(aCredentials.iccId,
|
||||
aCredentials.msisdn,
|
||||
aOrigin,
|
||||
aCredentials.sessionToken)
|
||||
.then(
|
||||
() => {
|
||||
deferred.resolve(assertion);
|
||||
}
|
||||
);
|
||||
});
|
||||
}, deferred.reject
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
getMobileIdAssertion: function(aPrincipal, aPromiseId) {
|
||||
log.debug("getMobileIdAssertion ${}", aPrincipal);
|
||||
|
||||
let uri = Services.io.newURI(aPrincipal.origin, null, null);
|
||||
let principal = securityManager.getAppCodebasePrincipal(
|
||||
uri, aPrincipal.appid, aPrincipal.isInBrowserElement);
|
||||
let manifestURL = appsService.getManifestURLByLocalId(aPrincipal.appId);
|
||||
|
||||
// First of all we look if we already have credentials for this origin.
|
||||
// If we don't have credentials it means that it is the first time that
|
||||
// the caller requested an assertion.
|
||||
return this.credStore.getByOrigin(aPrincipal.origin)
|
||||
.then(
|
||||
(creds) => {
|
||||
log.debug("creds ${creds} - ${origin}", { creds: creds,
|
||||
origin: aPrincipal.origin});
|
||||
if (!creds || !creds.sessionToken) {
|
||||
log.debug("No credentials");
|
||||
return;
|
||||
}
|
||||
|
||||
// It is possible that the ICC associated with the stored
|
||||
// credentials is not present in the device anymore, so we ask the
|
||||
// user if she still wants to use it anyway or if she prefers to use
|
||||
// another phone number.
|
||||
// If the credentials are associated with an external SIM or there is
|
||||
// no SIM in the device, we just return the credentials.
|
||||
if (this.iccInfo && creds.iccId) {
|
||||
for (let i = 0; i < this.iccInfo.length; i++) {
|
||||
if (this.iccInfo[i].iccId == creds.iccId) {
|
||||
return creds;
|
||||
}
|
||||
}
|
||||
// At this point we know that the SIM associated with the credentials
|
||||
// is not present in the device any more, so we need to ask the user
|
||||
// what to do.
|
||||
return this.promptAndVerify(principal, manifestURL, creds);
|
||||
}
|
||||
return creds;
|
||||
}
|
||||
)
|
||||
.then(
|
||||
(creds) => {
|
||||
// Even if we have credentails it is possible that the user has
|
||||
// removed the permission to share its mobile id with this origin, so
|
||||
// we check the permission and if it is not granted, we ask the user
|
||||
// before generating and sharing the assertion.
|
||||
// If we've just prompted the user in the previous step, the permission
|
||||
// is already granted and stored so we just progress the credentials.
|
||||
if (creds) {
|
||||
if (this.hasPermission(principal)) {
|
||||
return creds;
|
||||
}
|
||||
return this.promptAndVerify(principal, manifestURL, creds);
|
||||
}
|
||||
return this.promptAndVerify(principal, manifestURL);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
(creds) => {
|
||||
if (creds) {
|
||||
return this.generateAssertion(creds, principal.origin);
|
||||
}
|
||||
return Promise.reject(ERROR_INTERNAL_CANNOT_GENERATE_ASSERTION);
|
||||
}
|
||||
)
|
||||
.then(
|
||||
(assertion) => {
|
||||
if (!assertion) {
|
||||
return Promise.reject(ERROR_INTERNAL_CANNOT_GENERATE_ASSERTION);
|
||||
}
|
||||
|
||||
// Get the verified phone number from the assertion.
|
||||
let segments = assertion.split(".");
|
||||
if (!segments) {
|
||||
return Promise.reject(ERROR_INVALID_ASSERTION);
|
||||
}
|
||||
|
||||
// We need to translate the base64 alphabet used in JWT to our base64
|
||||
// alphabet before calling atob.
|
||||
let decodedPayload = JSON.parse(atob(segments[1].replace(/-/g, '+')
|
||||
.replace(/_/g, '/')));
|
||||
|
||||
if (!decodedPayload || !decodedPayload.verifiedMSISDN) {
|
||||
return Promise.reject(ERROR_INVALID_ASSERTION);
|
||||
}
|
||||
|
||||
this.ui.verified(decodedPayload.verifiedMSISDN);
|
||||
|
||||
let mm = this.messageManagers[aPromiseId];
|
||||
mm.sendAsyncMessage("MobileId:GetAssertion:Return:OK", {
|
||||
promiseId: aPromiseId,
|
||||
result: assertion
|
||||
});
|
||||
}
|
||||
)
|
||||
.then(
|
||||
null,
|
||||
(error) => {
|
||||
log.error("getMobileIdAssertion rejected with " + error);
|
||||
// Notify the error to the UI.
|
||||
this.ui.error(error);
|
||||
|
||||
let mm = this.messageManagers[aPromiseId];
|
||||
mm.sendAsyncMessage("MobileId:GetAssertion:Return:KO", {
|
||||
promiseId: aPromiseId,
|
||||
error: error
|
||||
});
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
MobileIdentityManager.init();
|
89
services/mobileid/MobileIdentitySmsMoMtVerificationFlow.jsm
Normal file
89
services/mobileid/MobileIdentitySmsMoMtVerificationFlow.jsm
Normal file
@ -0,0 +1,89 @@
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["MobileIdentitySmsMoMtVerificationFlow"];
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
|
||||
Cu.import("resource://gre/modules/MobileIdentitySmsVerificationFlow.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "smsService",
|
||||
"@mozilla.org/sms/smsservice;1",
|
||||
"nsISmsService");
|
||||
|
||||
// In order to send messages through nsISmsService, we need to implement
|
||||
// nsIMobileMessageCallback, as the WebSMS API implementation is not usable
|
||||
// from JS.
|
||||
function SilentSmsRequest(aDeferred) {
|
||||
this.deferred = aDeferred;
|
||||
}
|
||||
SilentSmsRequest.prototype = {
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileMessageCallback]),
|
||||
|
||||
classID: Components.ID("{ff46f1a8-e040-4ff4-98a7-d5a5b86a2c3e}"),
|
||||
|
||||
notifyMessageSent: function notifyMessageSent(aMessage) {
|
||||
log.debug("Silent message successfully sent");
|
||||
this.deferred.resolve(aMessage);
|
||||
},
|
||||
|
||||
notifySendMessageFailed: function notifySendMessageFailed(aError) {
|
||||
log.error("Error sending silent message " + aError);
|
||||
this.deferred.reject(aError);
|
||||
}
|
||||
};
|
||||
|
||||
this.MobileIdentitySmsMoMtVerificationFlow = function(aOrigin,
|
||||
aServiceId,
|
||||
aIccId,
|
||||
aMtSender,
|
||||
aMoVerifier,
|
||||
aUI,
|
||||
aClient) {
|
||||
|
||||
log.debug("MobileIdentitySmsMoMtVerificationFlow");
|
||||
|
||||
MobileIdentitySmsVerificationFlow.call(this,
|
||||
aOrigin,
|
||||
null, //msisdn
|
||||
aIccId,
|
||||
aServiceId,
|
||||
false, // external
|
||||
aMtSender,
|
||||
aMoVerifier,
|
||||
aUI,
|
||||
aClient,
|
||||
this.smsVerifyStrategy);
|
||||
};
|
||||
|
||||
this.MobileIdentitySmsMoMtVerificationFlow.prototype = {
|
||||
|
||||
__proto__: MobileIdentitySmsVerificationFlow.prototype,
|
||||
|
||||
smsVerifyStrategy: function() {
|
||||
// In the MO+MT flow we need to send an SMS to the given moVerifier number
|
||||
// so the server can find out our phone number to send an SMS back with a
|
||||
// verification code.
|
||||
let deferred = Promise.defer();
|
||||
let silentSmsRequest = new SilentSmsRequest(deferred);
|
||||
|
||||
// The MO SMS body that the server expects contains the API endpoint for
|
||||
// the MO verify request and the HAWK ID parameter derived via HKDF from
|
||||
// the session token. These parameters should go unnamed and space limited.
|
||||
let body = SMS_MO_MT_VERIFY + " " +
|
||||
this.client._deriveHawkCredentials(this.sessionToken).id;
|
||||
smsService.send(this.verificationOptions.serviceId,
|
||||
this.verificationOptions.moVerifier,
|
||||
body,
|
||||
true, // silent
|
||||
silentSmsRequest);
|
||||
log.debug("Sending " + body + " to " + this.verificationOptions.moVerifier);
|
||||
return deferred.promise;
|
||||
}
|
||||
};
|
49
services/mobileid/MobileIdentitySmsMtVerificationFlow.jsm
Normal file
49
services/mobileid/MobileIdentitySmsMtVerificationFlow.jsm
Normal file
@ -0,0 +1,49 @@
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["MobileIdentitySmsMtVerificationFlow"];
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
|
||||
Cu.import("resource://gre/modules/MobileIdentitySmsVerificationFlow.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
this.MobileIdentitySmsMtVerificationFlow = function(aOrigin,
|
||||
aMsisdn,
|
||||
aIccId,
|
||||
aExternal,
|
||||
aMtSender,
|
||||
aUI,
|
||||
aClient) {
|
||||
|
||||
log.debug("MobileIdentitySmsVerificationFlow " + aMsisdn + ", external: " +
|
||||
aExternal);
|
||||
|
||||
MobileIdentitySmsVerificationFlow.call(this,
|
||||
aOrigin,
|
||||
aMsisdn,
|
||||
aIccId,
|
||||
null, // service ID
|
||||
aExternal,
|
||||
aMtSender,
|
||||
null, // moVerifier
|
||||
aUI,
|
||||
aClient,
|
||||
this.smsVerifyStrategy);
|
||||
};
|
||||
|
||||
this.MobileIdentitySmsMtVerificationFlow.prototype = {
|
||||
|
||||
__proto__: MobileIdentitySmsVerificationFlow.prototype,
|
||||
|
||||
smsVerifyStrategy: function() {
|
||||
return this.client.smsMtVerify(this.sessionToken,
|
||||
this.verificationOptions.msisdn,
|
||||
this.verificationOptions.external);
|
||||
}
|
||||
};
|
120
services/mobileid/MobileIdentitySmsVerificationFlow.jsm
Normal file
120
services/mobileid/MobileIdentitySmsVerificationFlow.jsm
Normal file
@ -0,0 +1,120 @@
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["MobileIdentitySmsVerificationFlow"];
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
|
||||
Cu.import("resource://gre/modules/MobileIdentityVerificationFlow.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
#ifdef MOZ_B2G_RIL
|
||||
XPCOMUtils.defineLazyServiceGetter(this, "smsService",
|
||||
"@mozilla.org/sms/smsservice;1",
|
||||
"nsISmsService");
|
||||
#endif
|
||||
|
||||
this.MobileIdentitySmsVerificationFlow = function(aOrigin,
|
||||
aMsisdn,
|
||||
aIccId,
|
||||
aServiceId,
|
||||
aExternal,
|
||||
aMtSender,
|
||||
aMoVerifier,
|
||||
aUI,
|
||||
aClient,
|
||||
aVerifyStrategy) {
|
||||
|
||||
// SMS MT or SMS MO+MT specific verify strategy.
|
||||
this.smsVerifyStrategy = aVerifyStrategy;
|
||||
|
||||
MobileIdentityVerificationFlow.call(this, {
|
||||
origin: aOrigin,
|
||||
msisdn: aMsisdn,
|
||||
iccId: aIccId,
|
||||
serviceId: aServiceId,
|
||||
external: aExternal,
|
||||
mtSender: aMtSender,
|
||||
moVerifier: aMoVerifier
|
||||
}, aUI, aClient, this._verifyStrategy, this._cleanupStrategy);
|
||||
};
|
||||
|
||||
this.MobileIdentitySmsVerificationFlow.prototype = {
|
||||
|
||||
__proto__: MobileIdentityVerificationFlow.prototype,
|
||||
|
||||
observedSilentNumber: null,
|
||||
|
||||
onSilentSms: null,
|
||||
|
||||
_verifyStrategy: function() {
|
||||
if (!this.smsVerifyStrategy) {
|
||||
return Promise.reject(ERROR_INTERNAL_UNEXPECTED);
|
||||
}
|
||||
|
||||
// Even if the user selection is given to us as a possible external phone
|
||||
// number, it is also possible that the phone number introduced by the
|
||||
// user belongs to one of the SIMs inserted in the device which MSISDN
|
||||
// is unknown for us, so we always observe for incoming messages coming
|
||||
// from the given mtSender.
|
||||
|
||||
#ifdef MOZ_B2G_RIL
|
||||
this.observedSilentNumber = this.verificationOptions.mtSender;
|
||||
try {
|
||||
smsService.addSilentNumber(this.observedSilentNumber);
|
||||
} catch (e) {
|
||||
log.warn("We are already listening for that number");
|
||||
}
|
||||
|
||||
this.onSilentSms = (function(aSubject, aTopic, aData) {
|
||||
log.debug("Got silent message " + aSubject.sender + " - " + aSubject.body);
|
||||
// We might have observed a notification of an incoming silent message
|
||||
// for other number. In that case, we just bail out.
|
||||
if (aSubject.sender != this.observedSilentNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We got the SMS containing the verification code.
|
||||
|
||||
// If the phone number we are trying to verify is or can be an external
|
||||
// phone number (meaning that it doesn't belong to any of the inserted
|
||||
// SIMs) we will be receiving an human readable SMS containing a short
|
||||
// verification code. In this case we need to parse the SMS body to
|
||||
// extract the verification code.
|
||||
// Otherwise, we just use the whole SMS body as it should contain a long
|
||||
// verification code.
|
||||
let verificationCode = aSubject.body;
|
||||
if (this.verificationOptions.external) {
|
||||
// We just take the numerical characters from the body.
|
||||
verificationCode = aSubject.body.replace(/[^0-9]/g,'');
|
||||
}
|
||||
|
||||
log.debug("Verification code: " + verificationCode);
|
||||
|
||||
this.verificationCodeDeferred.resolve(verificationCode);
|
||||
}).bind(this);
|
||||
|
||||
Services.obs.addObserver(this.onSilentSms,
|
||||
SILENT_SMS_RECEIVED_TOPIC,
|
||||
false);
|
||||
log.debug("Observing messages from " + this.observedSilentNumber);
|
||||
#endif
|
||||
|
||||
return this.smsVerifyStrategy();
|
||||
},
|
||||
|
||||
_cleanupStrategy: function() {
|
||||
#ifdef MOZ_B2G_RIL
|
||||
smsService.removeSilentNumber(this.observedSilentNumber);
|
||||
Services.obs.removeObserver(this.onSilentSms,
|
||||
SILENT_SMS_RECEIVED_TOPIC);
|
||||
this.observedSilentNumber = null;
|
||||
this.onSilentSms = null;
|
||||
#endif
|
||||
}
|
||||
};
|
31
services/mobileid/MobileIdentityUIGlueCommon.jsm
Normal file
31
services/mobileid/MobileIdentityUIGlueCommon.jsm
Normal file
@ -0,0 +1,31 @@
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["MobileIdentityUIGluePhoneInfo",
|
||||
"MobileIdentityUIGluePromptResult"];
|
||||
|
||||
this.MobileIdentityUIGluePhoneInfo = function (aMsisdn, aOperator, aServiceId,
|
||||
aExternal, aPrimary) {
|
||||
this.msisdn = aMsisdn;
|
||||
this.operator = aOperator;
|
||||
this.serviceId = aServiceId;
|
||||
// A phone number is considered "external" when it doesn't or we don't know
|
||||
// if it does belong to any of the device SIM cards.
|
||||
this.external = aExternal;
|
||||
this.primary = aPrimary;
|
||||
}
|
||||
|
||||
this.MobileIdentityUIGluePhoneInfo.prototype = {};
|
||||
|
||||
this.MobileIdentityUIGluePromptResult = function (aPhoneNumber, aPrefix, aMcc,
|
||||
aServiceId) {
|
||||
this.phoneNumber = aPhoneNumber;
|
||||
this.prefix = aPrefix;
|
||||
this.mcc = aMcc;
|
||||
this.serviceId = aServiceId;
|
||||
}
|
||||
|
||||
this.MobileIdentityUIGluePromptResult.prototype = {};
|
213
services/mobileid/MobileIdentityVerificationFlow.jsm
Normal file
213
services/mobileid/MobileIdentityVerificationFlow.jsm
Normal file
@ -0,0 +1,213 @@
|
||||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["MobileIdentityVerificationFlow"];
|
||||
|
||||
const { utils: Cu, classes: Cc, interfaces: Ci } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/MobileIdentityCommon.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
this.MobileIdentityVerificationFlow = function(aVerificationOptions,
|
||||
aUI,
|
||||
aClient,
|
||||
aVerifyStrategy,
|
||||
aCleanupStrategy) {
|
||||
this.verificationOptions = aVerificationOptions;
|
||||
this.ui = aUI;
|
||||
this.client = aClient;
|
||||
this.retries = VERIFICATIONCODE_RETRIES;
|
||||
this.verifyStrategy = aVerifyStrategy;
|
||||
this.cleanupStrategy = aCleanupStrategy;
|
||||
};
|
||||
|
||||
MobileIdentityVerificationFlow.prototype = {
|
||||
|
||||
doVerification: function() {
|
||||
log.debug("Start verification flow");
|
||||
return this.register()
|
||||
.then(
|
||||
(registerResult) => {
|
||||
log.debug("Register result ${}", registerResult);
|
||||
if (!registerResult || !registerResult.msisdnSessionToken) {
|
||||
return Promise.reject(ERROR_INTERNAL_UNEXPECTED);
|
||||
}
|
||||
this.sessionToken = registerResult.msisdnSessionToken;
|
||||
return this._doVerification();
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
_doVerification: function() {
|
||||
log.debug("_doVerification");
|
||||
// We save the timestamp of the start of the verification timeout to be
|
||||
// able to provide to the UI the remaining time on each retry.
|
||||
if (!this.timer) {
|
||||
log.debug("Creating verification code timer");
|
||||
this.timerCreation = Date.now();
|
||||
this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
this.timer.initWithCallback(this.onVerificationCodeTimeout.bind(this),
|
||||
VERIFICATIONCODE_TIMEOUT,
|
||||
this.timer.TYPE_ONE_SHOT);
|
||||
}
|
||||
|
||||
if (!this.verifyStrategy) {
|
||||
return Promise.reject(ERROR_INTERNAL_INVALID_VERIFICATION_FLOW);
|
||||
}
|
||||
|
||||
this.verificationCodeDeferred = Promise.defer();
|
||||
|
||||
this.verifyStrategy()
|
||||
.then(
|
||||
() => {
|
||||
// If the verification flow can be for an external phone number,
|
||||
// we need to ask the user for the verification code.
|
||||
// In that case we don't do a notification about the verification
|
||||
// process being done until the user enters the verification code
|
||||
// in the UI.
|
||||
if (this.verificationOptions.external) {
|
||||
let timeLeft = 0;
|
||||
if (this.timer) {
|
||||
timeLeft = this.timerCreation + VERIFICATIONCODE_TIMEOUT -
|
||||
Date.now();
|
||||
}
|
||||
this.ui.verificationCodePrompt(this.retries,
|
||||
VERIFICATIONCODE_TIMEOUT / 1000,
|
||||
timeLeft / 1000)
|
||||
.then(
|
||||
(verificationCode) => {
|
||||
if (!verificationCode) {
|
||||
return this.verificationCodeDeferred.reject(
|
||||
ERROR_INTERNAL_INVALID_PROMPT_RESULT);
|
||||
}
|
||||
// If the user got the verification code that means that the
|
||||
// introduced phone number didn't belong to any of the inserted
|
||||
// SIMs.
|
||||
this.ui.verify();
|
||||
this.verificationCodeDeferred.resolve(verificationCode);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.ui.verify();
|
||||
}
|
||||
},
|
||||
(reason) => {
|
||||
this.verificationCodeDeferred.reject(reason);
|
||||
}
|
||||
);
|
||||
return this.verificationCodeDeferred.promise.then(
|
||||
this.onVerificationCode.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
// When we receive a verification code from the UI, we check it against
|
||||
// the server. If the verification code is incorrect, we decrease the
|
||||
// number of retries left and allow the user to try again. If there is no
|
||||
// possible retry left, we notify about this error so the UI can allow the
|
||||
// user to request the resend of a new verification code.
|
||||
onVerificationCode: function(aVerificationCode) {
|
||||
log.debug("onVerificationCode " + aVerificationCode);
|
||||
if (!aVerificationCode) {
|
||||
this.ui.error(ERROR_INVALID_VERIFICATION_CODE);
|
||||
return this._doVerification();
|
||||
}
|
||||
|
||||
// Before checking the verification code against the server we set the
|
||||
// "verifying" flag to queue timeout expiration events received before
|
||||
// the server request is completed. If the server request is positive
|
||||
// we will discard the timeout event, otherwise we will progress the
|
||||
// event to the UI to allow the user to retry.
|
||||
this.verifying = true;
|
||||
|
||||
return this.verifyCode(aVerificationCode)
|
||||
.then(
|
||||
(result) => {
|
||||
if (!result) {
|
||||
return Promise.reject(INTERNAL_UNEXPECTED);
|
||||
}
|
||||
// The code was correct!
|
||||
// At this point the phone number is verified.
|
||||
// We return the given verification options with the session token
|
||||
// to be stored in the credentials store. With this data we will be
|
||||
// asking the server to give us a certificate to generate assertions.
|
||||
this.verificationOptions.sessionToken = this.sessionToken;
|
||||
this.verificationOptions.msisdn = result.msisdn ||
|
||||
this.verificationOptions.msisdn;
|
||||
return this.verificationOptions;
|
||||
},
|
||||
(error) => {
|
||||
log.error("Verification code error " + error);
|
||||
this.retries--;
|
||||
log.error("Retries left " + this.retries);
|
||||
if (!this.retries) {
|
||||
this.ui.error(ERROR_NO_RETRIES_LEFT);
|
||||
return Promise.reject(ERROR_NO_RETRIES_LEFT);
|
||||
}
|
||||
this.verifying = false;
|
||||
if (this.queuedTimeout) {
|
||||
this.onVerificationCodeTimeout();
|
||||
}
|
||||
return this._doVerification();
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
onVerificationCodeTimeout: function() {
|
||||
// It is possible that we get the timeout when we are checking a
|
||||
// verification code with the server. In that case, we queue the
|
||||
// timeout to be triggered after we receive the reply from the server
|
||||
// if needed.
|
||||
if (this.verifying) {
|
||||
this.queuedTimeout = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// When the verification process times out we do a clean up, reject
|
||||
// the corresponding promise and notify the UI about the timeout.
|
||||
if (this.verificationCodeDeferred) {
|
||||
this.verificationCodeDeferred.reject(ERROR_VERIFICATION_CODE_TIMEOUT);
|
||||
}
|
||||
this.ui.error(ERROR_VERIFICATION_CODE_TIMEOUT);
|
||||
},
|
||||
|
||||
register: function() {
|
||||
return this.client.register();
|
||||
},
|
||||
|
||||
verifyCode: function(aVerificationCode) {
|
||||
return this.client.verifyCode(this.sessionToken, aVerificationCode);
|
||||
},
|
||||
|
||||
unregister: function() {
|
||||
return this.client.unregister(this.sessionToken);
|
||||
},
|
||||
|
||||
cleanup: function(aUnregister = false) {
|
||||
log.debug("Verification flow cleanup");
|
||||
|
||||
this.queuedTimeout = false;
|
||||
this.retries = VERIFICATIONCODE_RETRIES;
|
||||
|
||||
if (this.timer) {
|
||||
this.timer.cancel();
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
if (aUnregister) {
|
||||
this.unregister().
|
||||
then(
|
||||
() => {
|
||||
this.sessionToken = null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (this.cleanupStrategy) {
|
||||
this.cleanupStrategy();
|
||||
}
|
||||
}
|
||||
};
|
11
services/mobileid/interfaces/moz.build
Normal file
11
services/mobileid/interfaces/moz.build
Normal file
@ -0,0 +1,11 @@
|
||||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
XPIDL_SOURCES += [
|
||||
'nsIMobileIdentityUIGlue.idl'
|
||||
]
|
||||
|
||||
XPIDL_MODULE = 'services_mobileidentity'
|
77
services/mobileid/interfaces/nsIMobileIdentityUIGlue.idl
Normal file
77
services/mobileid/interfaces/nsIMobileIdentityUIGlue.idl
Normal file
@ -0,0 +1,77 @@
|
||||
/* 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/. */
|
||||
|
||||
#include "nsISupports.idl"
|
||||
|
||||
[scriptable, uuid(6c4c5758-e041-4e0d-98da-67bb552f8018)]
|
||||
interface nsIMobileIdentityUIGlue : nsISupports
|
||||
{
|
||||
/**
|
||||
* Request the creation of a Mobile ID UI flow.
|
||||
*
|
||||
* The permission prompt starts the verification flow asking the user
|
||||
* for permission to share her phone number and allowing her to choose
|
||||
* an already known phone number, a SIM which phone number is unknown
|
||||
* (even in a multi-SIM scenario) or an external phone number.
|
||||
* Selecting a phone number implies giving permission to share it with the
|
||||
* API caller, so the UI should be clear about this.
|
||||
*
|
||||
* @manifestURL manifest URL of the mobile ID requester.
|
||||
* @iccInfo array of objects containing the information about the
|
||||
* SIM cards available in the device and that can be used for the
|
||||
* phone number verification and share process.
|
||||
*
|
||||
* Returns a Promise. An instance of nsIMobileIdentityUIGluePromptResult will
|
||||
* be returned as result of the Promise or a single string containing an error
|
||||
* in case of rejection.
|
||||
*/
|
||||
jsval startFlow(in DOMString manifestURL, in jsval iccInfo);
|
||||
|
||||
/**
|
||||
* Will prompt the user to enter a code used to verify a phone number.
|
||||
* This will only be called if an external phone number is selected in
|
||||
* startFlow().
|
||||
*
|
||||
* @retries number of retries left to validate a verification code.
|
||||
* @timeout the verification code expires after the timeout fires. This is
|
||||
* the total life time of the verification code.
|
||||
* @timeLeft we might call verificationCodePrompt more than once for the
|
||||
* same verification flow (i.e. when the verification code entered
|
||||
* by the user is incorrect) so we give to the UI the amount of
|
||||
* time left before the verification code expires.
|
||||
*
|
||||
* Returns a Promise. The value of the resolved promise will be the
|
||||
* verification code introduced through the UI or an error in case of
|
||||
* rejection of the promise.
|
||||
*/
|
||||
jsval verificationCodePrompt(in short retries,
|
||||
in long timeout,
|
||||
in long timeLeft);
|
||||
|
||||
/**
|
||||
* Notify the UI about the start of the verification process.
|
||||
*/
|
||||
void verify();
|
||||
|
||||
/**
|
||||
* Notify the UI about an error in the verification process.
|
||||
*/
|
||||
void error(in DOMString error);
|
||||
|
||||
/**
|
||||
* Notify the UI about the succesful phone number verification.
|
||||
*/
|
||||
void verified(in DOMString verifiedPhoneNumber);
|
||||
|
||||
/**
|
||||
* Callback to be called when the user cancels the verification flow via UI.
|
||||
*/
|
||||
attribute jsval oncancel;
|
||||
|
||||
/**
|
||||
* Callback to be called when the user requests a resend of a verification
|
||||
* code.
|
||||
*/
|
||||
attribute jsval onresendcode;
|
||||
};
|
22
services/mobileid/moz.build
Normal file
22
services/mobileid/moz.build
Normal file
@ -0,0 +1,22 @@
|
||||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
PARALLEL_DIRS += ['interfaces']
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'MobileIdentityClient.jsm',
|
||||
'MobileIdentityCommon.jsm',
|
||||
'MobileIdentityCredentialsStore.jsm',
|
||||
'MobileIdentitySmsMoMtVerificationFlow.jsm',
|
||||
'MobileIdentitySmsMtVerificationFlow.jsm',
|
||||
'MobileIdentityUIGlueCommon.jsm',
|
||||
'MobileIdentityVerificationFlow.jsm'
|
||||
]
|
||||
|
||||
EXTRA_PP_JS_MODULES += [
|
||||
'MobileIdentityManager.jsm',
|
||||
'MobileIdentitySmsVerificationFlow.jsm'
|
||||
]
|
@ -6,7 +6,7 @@
|
||||
|
||||
PARALLEL_DIRS += [
|
||||
'common',
|
||||
'crypto',
|
||||
'crypto'
|
||||
]
|
||||
|
||||
if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android':
|
||||
@ -27,4 +27,7 @@ if CONFIG['MOZ_SERVICES_FXACCOUNTS']:
|
||||
if CONFIG['MOZ_SERVICES_SYNC']:
|
||||
PARALLEL_DIRS += ['sync']
|
||||
|
||||
if CONFIG['MOZ_B2G']:
|
||||
PARALLEL_DIRS += ['mobileid']
|
||||
|
||||
SPHINX_TREES['services'] = 'docs'
|
||||
|
@ -27,3 +27,6 @@ skip = false
|
||||
[include:../../../../../dom/nfc/tests/marionette/manifest.ini]
|
||||
[include:../../../../../dom/events/test/marionette/manifest.ini]
|
||||
[include:../../../../../dom/wifi/test/marionette/manifest.ini]
|
||||
|
||||
; layout tests
|
||||
[include:../../../../../layout/base/tests/marionette/manifest.ini]
|
||||
|
@ -0,0 +1,17 @@
|
||||
<!-- 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/. -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html id="html">
|
||||
<head>
|
||||
<title>Bug 960897: Marionette tests for touch caret</title>
|
||||
</head>
|
||||
<body>
|
||||
<div><input id="input" value="ABCDEFGHI"></input></div>
|
||||
<br />
|
||||
<div><textarea name="textarea" id="textarea" rows="4" cols="6">ABCDEFGHI</textarea></div>
|
||||
<br />
|
||||
<div style="width: 10em; height: 2em; word-wrap: break-word; overflow: auto;" contenteditable="true" id="contenteditable">ABCDEFGHI</div>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user