diff options
Diffstat (limited to 'b2g/components')
81 files changed, 10537 insertions, 0 deletions
diff --git a/b2g/components/AboutServiceWorkers.jsm b/b2g/components/AboutServiceWorkers.jsm new file mode 100644 index 000000000..fe67e9c34 --- /dev/null +++ b/b2g/components/AboutServiceWorkers.jsm @@ -0,0 +1,183 @@ +/* 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 = ["AboutServiceWorkers"]; + +const { interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy", + "resource://gre/modules/SystemAppProxy.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "gServiceWorkerManager", + "@mozilla.org/serviceworkers/manager;1", + "nsIServiceWorkerManager"); + +function debug(aMsg) { + dump("AboutServiceWorkers - " + aMsg + "\n"); +} + +function serializeServiceWorkerInfo(aServiceWorkerInfo) { + if (!aServiceWorkerInfo) { + throw new Error("Invalid service worker information"); + } + + let result = {}; + + result.principal = { + origin: aServiceWorkerInfo.principal.originNoSuffix, + originAttributes: aServiceWorkerInfo.principal.originAttributes + }; + + ["scope", "scriptSpec"].forEach(property => { + result[property] = aServiceWorkerInfo[property]; + }); + + return result; +} + + +this.AboutServiceWorkers = { + get enabled() { + if (this._enabled) { + return this._enabled; + } + this._enabled = false; + try { + this._enabled = Services.prefs.getBoolPref("dom.serviceWorkers.enabled"); + } catch(e) {} + return this._enabled; + }, + + init: function() { + SystemAppProxy.addEventListener("mozAboutServiceWorkersContentEvent", + AboutServiceWorkers); + }, + + sendResult: function(aId, aResult) { + SystemAppProxy._sendCustomEvent("mozAboutServiceWorkersChromeEvent", { + id: aId, + result: aResult + }); + }, + + sendError: function(aId, aError) { + SystemAppProxy._sendCustomEvent("mozAboutServiceWorkersChromeEvent", { + id: aId, + error: aError + }); + }, + + handleEvent: function(aEvent) { + let message = aEvent.detail; + + debug("Got content event " + JSON.stringify(message)); + + if (!message.id || !message.name) { + dump("Invalid event " + JSON.stringify(message) + "\n"); + return; + } + + let self = AboutServiceWorkers; + + switch(message.name) { + case "init": + if (!self.enabled) { + self.sendResult(message.id, { + enabled: false, + registrations: [] + }); + return; + }; + + let data = gServiceWorkerManager.getAllRegistrations(); + if (!data) { + self.sendError(message.id, "NoServiceWorkersRegistrations"); + return; + } + + let registrations = []; + + for (let i = 0; i < data.length; i++) { + let info = data.queryElementAt(i, Ci.nsIServiceWorkerRegistrationInfo); + if (!info) { + dump("AboutServiceWorkers: Invalid nsIServiceWorkerRegistrationInfo " + + "interface.\n"); + continue; + } + registrations.push(serializeServiceWorkerInfo(info)); + } + + self.sendResult(message.id, { + enabled: self.enabled, + registrations: registrations + }); + break; + + case "update": + if (!message.scope) { + self.sendError(message.id, "MissingScope"); + return; + } + + if (!message.principal || + !message.principal.originAttributes) { + self.sendError(message.id, "MissingOriginAttributes"); + return; + } + + gServiceWorkerManager.propagateSoftUpdate( + message.principal.originAttributes, + message.scope + ); + + self.sendResult(message.id, true); + break; + + case "unregister": + if (!message.principal || + !message.principal.origin || + !message.principal.originAttributes || + !message.principal.originAttributes.appId || + (message.principal.originAttributes.inIsolatedMozBrowser == null)) { + self.sendError(message.id, "MissingPrincipal"); + return; + } + + let principal = Services.scriptSecurityManager.createCodebasePrincipal( + // TODO: Bug 1196652. use originNoSuffix + Services.io.newURI(message.principal.origin, null, null), + message.principal.originAttributes); + + if (!message.scope) { + self.sendError(message.id, "MissingScope"); + return; + } + + let serviceWorkerUnregisterCallback = { + unregisterSucceeded: function() { + self.sendResult(message.id, true); + }, + + unregisterFailed: function() { + self.sendError(message.id, "UnregisterError"); + }, + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIServiceWorkerUnregisterCallback + ]) + }; + gServiceWorkerManager.propagateUnregister(principal, + serviceWorkerUnregisterCallback, + message.scope); + break; + } + } +}; + +AboutServiceWorkers.init(); diff --git a/b2g/components/ActivityChannel.jsm b/b2g/components/ActivityChannel.jsm new file mode 100644 index 000000000..9dfa13d67 --- /dev/null +++ b/b2g/components/ActivityChannel.jsm @@ -0,0 +1,64 @@ +/* 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, results: Cr} = Components; + +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); + +XPCOMUtils.defineLazyServiceGetter(this, "cpmm", + "@mozilla.org/childprocessmessagemanager;1", + "nsIMessageSender"); + +XPCOMUtils.defineLazyServiceGetter(this, "contentSecManager", + "@mozilla.org/contentsecuritymanager;1", + "nsIContentSecurityManager"); + +this.EXPORTED_SYMBOLS = ["ActivityChannel"]; + +this.ActivityChannel = function(aURI, aLoadInfo, aName, aDetails) { + this._activityName = aName; + this._activityDetails = aDetails; + this.originalURI = aURI; + this.URI = aURI; + this.loadInfo = aLoadInfo; +} + +this.ActivityChannel.prototype = { + originalURI: null, + URI: null, + owner: null, + notificationCallbacks: null, + securityInfo: null, + contentType: null, + contentCharset: null, + contentLength: 0, + contentDisposition: Ci.nsIChannel.DISPOSITION_INLINE, + contentDispositionFilename: null, + contentDispositionHeader: null, + loadInfo: null, + + open: function() { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + open2: function() { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + }, + + asyncOpen: function(aListener, aContext) { + cpmm.sendAsyncMessage(this._activityName, this._activityDetails); + // Let the listener cleanup. + aListener.onStopRequest(this, aContext, Cr.NS_OK); + }, + + asyncOpen2: function(aListener) { + // throws an error if security checks fail + var outListener = contentSecManager.performSecurityCheck(this, aListener); + this.asyncOpen(outListener, null); + }, + + QueryInterface2: XPCOMUtils.generateQI([Ci.nsIChannel]) +} diff --git a/b2g/components/AlertsHelper.jsm b/b2g/components/AlertsHelper.jsm new file mode 100644 index 000000000..820f2406c --- /dev/null +++ b/b2g/components/AlertsHelper.jsm @@ -0,0 +1,279 @@ +/* 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 Ci = Components.interfaces; +const Cu = Components.utils; +const Cc = Components.classes; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/AppsUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "gSystemMessenger", + "@mozilla.org/system-message-internal;1", + "nsISystemMessagesInternal"); + +XPCOMUtils.defineLazyServiceGetter(this, "appsService", + "@mozilla.org/AppsService;1", + "nsIAppsService"); + +XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy", + "resource://gre/modules/SystemAppProxy.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "notificationStorage", + "@mozilla.org/notificationStorage;1", + "nsINotificationStorage"); + +XPCOMUtils.defineLazyGetter(this, "ppmm", function() { + return Cc["@mozilla.org/parentprocessmessagemanager;1"] + .getService(Ci.nsIMessageListenerManager); +}); + +function debug(str) { + //dump("=*= AlertsHelper.jsm : " + str + "\n"); +} + +const kNotificationIconSize = 128; + +const kDesktopNotificationPerm = "desktop-notification"; + +const kNotificationSystemMessageName = "notification"; + +const kDesktopNotification = "desktop-notification"; +const kDesktopNotificationShow = "desktop-notification-show"; +const kDesktopNotificationClick = "desktop-notification-click"; +const kDesktopNotificationClose = "desktop-notification-close"; + +const kTopicAlertClickCallback = "alertclickcallback"; +const kTopicAlertShow = "alertshow"; +const kTopicAlertFinished = "alertfinished"; + +const kMozChromeNotificationEvent = "mozChromeNotificationEvent"; +const kMozContentNotificationEvent = "mozContentNotificationEvent"; + +const kMessageAlertNotificationSend = "alert-notification-send"; +const kMessageAlertNotificationClose = "alert-notification-close"; + +const kMessages = [ + kMessageAlertNotificationSend, + kMessageAlertNotificationClose +]; + +var AlertsHelper = { + + _listeners: {}, + + init: function() { + Services.obs.addObserver(this, "xpcom-shutdown", false); + for (let message of kMessages) { + ppmm.addMessageListener(message, this); + } + SystemAppProxy.addEventListener(kMozContentNotificationEvent, this); + }, + + observe: function(aSubject, aTopic, aData) { + switch (aTopic) { + case "xpcom-shutdown": + Services.obs.removeObserver(this, "xpcom-shutdown"); + for (let message of kMessages) { + ppmm.removeMessageListener(message, this); + } + SystemAppProxy.removeEventListener(kMozContentNotificationEvent, this); + break; + } + }, + + handleEvent: function(evt) { + let detail = evt.detail; + + switch(detail.type) { + case kDesktopNotificationShow: + case kDesktopNotificationClick: + case kDesktopNotificationClose: + this.handleNotificationEvent(detail); + break; + default: + debug("FIXME: Unhandled notification event: " + detail.type); + break; + } + }, + + handleNotificationEvent: function(detail) { + if (!detail || !detail.id) { + return; + } + + let uid = detail.id; + let listener = this._listeners[uid]; + if (!listener) { + return; + } + + let topic; + if (detail.type === kDesktopNotificationClick) { + topic = kTopicAlertClickCallback; + } else if (detail.type === kDesktopNotificationShow) { + topic = kTopicAlertShow; + } else { + /* kDesktopNotificationClose */ + topic = kTopicAlertFinished; + } + + if (listener.cookie) { + try { + listener.observer.observe(null, topic, listener.cookie); + } catch (e) { } + } else { + if (detail.type === kDesktopNotificationClose && listener.dbId) { + notificationStorage.delete(listener.manifestURL, listener.dbId); + } + } + + // we"re done with this notification + if (detail.type === kDesktopNotificationClose) { + delete this._listeners[uid]; + } + }, + + registerListener: function(alertId, cookie, alertListener) { + this._listeners[alertId] = { observer: alertListener, cookie: cookie }; + }, + + registerAppListener: function(uid, listener) { + this._listeners[uid] = listener; + + appsService.getManifestFor(listener.manifestURL).then((manifest) => { + let app = appsService.getAppByManifestURL(listener.manifestURL); + let helper = new ManifestHelper(manifest, app.origin, app.manifestURL); + let getNotificationURLFor = function(messages) { + if (!messages) { + return null; + } + + for (let i = 0; i < messages.length; i++) { + let message = messages[i]; + if (message === kNotificationSystemMessageName) { + return helper.fullLaunchPath(); + } else if (typeof message === "object" && + kNotificationSystemMessageName in message) { + return helper.resolveURL(message[kNotificationSystemMessageName]); + } + } + + // No message found... + return null; + } + + listener.target = getNotificationURLFor(manifest.messages); + + // Bug 816944 - Support notification messages for entry_points. + }); + }, + + deserializeStructuredClone: function(dataString) { + if (!dataString) { + return null; + } + let scContainer = Cc["@mozilla.org/docshell/structured-clone-container;1"]. + createInstance(Ci.nsIStructuredCloneContainer); + + // The maximum supported structured-clone serialization format version + // as defined in "js/public/StructuredClone.h" + let JS_STRUCTURED_CLONE_VERSION = 4; + scContainer.initFromBase64(dataString, JS_STRUCTURED_CLONE_VERSION); + let dataObj = scContainer.deserializeToVariant(); + + // We have to check whether dataObj contains DOM objects (supported by + // nsIStructuredCloneContainer, but not by Cu.cloneInto), e.g. ImageData. + // After the structured clone callback systems will be unified, we'll not + // have to perform this check anymore. + try { + let data = Cu.cloneInto(dataObj, {}); + } catch(e) { dataObj = null; } + + return dataObj; + }, + + showNotification: function(imageURL, title, text, textClickable, cookie, + uid, dir, lang, dataObj, manifestURL, timestamp, + behavior) { + function send(appName, appIcon) { + SystemAppProxy._sendCustomEvent(kMozChromeNotificationEvent, { + type: kDesktopNotification, + id: uid, + icon: imageURL, + title: title, + text: text, + dir: dir, + lang: lang, + appName: appName, + appIcon: appIcon, + manifestURL: manifestURL, + timestamp: timestamp, + data: dataObj, + mozbehavior: behavior + }); + } + + if (!manifestURL || !manifestURL.length) { + send(null, null); + return; + } + + // If we have a manifest URL, get the icon and title from the manifest + // to prevent spoofing. + appsService.getManifestFor(manifestURL).then((manifest) => { + let app = appsService.getAppByManifestURL(manifestURL); + let helper = new ManifestHelper(manifest, app.origin, manifestURL); + send(helper.name, helper.iconURLForSize(kNotificationIconSize)); + }); + }, + + showAlertNotification: function(aMessage) { + let data = aMessage.data; + let currentListener = this._listeners[data.name]; + if (currentListener && currentListener.observer) { + currentListener.observer.observe(null, kTopicAlertFinished, currentListener.cookie); + } + + let dataObj = this.deserializeStructuredClone(data.dataStr); + this.registerListener(data.name, data.cookie, data.alertListener); + this.showNotification(data.imageURL, data.title, data.text, + data.textClickable, data.cookie, data.name, data.dir, + data.lang, dataObj, null, data.inPrivateBrowsing); + }, + + closeAlert: function(name) { + SystemAppProxy._sendCustomEvent(kMozChromeNotificationEvent, { + type: kDesktopNotificationClose, + id: name + }); + }, + + receiveMessage: function(aMessage) { + if (!aMessage.target.assertAppHasPermission(kDesktopNotificationPerm)) { + Cu.reportError("Desktop-notification message " + aMessage.name + + " from a content process with no " + kDesktopNotificationPerm + + " privileges."); + return; + } + + switch(aMessage.name) { + case kMessageAlertNotificationSend: + this.showAlertNotification(aMessage); + break; + + case kMessageAlertNotificationClose: + this.closeAlert(aMessage.data.name); + break; + } + + }, +} + +AlertsHelper.init(); diff --git a/b2g/components/AlertsService.js b/b2g/components/AlertsService.js new file mode 100644 index 000000000..19a164f0e --- /dev/null +++ b/b2g/components/AlertsService.js @@ -0,0 +1,153 @@ +/* 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 Ci = Components.interfaces; +const Cu = Components.utils; +const Cc = Components.classes; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "gSystemMessenger", + "@mozilla.org/system-message-internal;1", + "nsISystemMessagesInternal"); + +XPCOMUtils.defineLazyServiceGetter(this, "uuidGenerator", + "@mozilla.org/uuid-generator;1", + "nsIUUIDGenerator"); + +XPCOMUtils.defineLazyServiceGetter(this, "notificationStorage", + "@mozilla.org/notificationStorage;1", + "nsINotificationStorage"); + +XPCOMUtils.defineLazyGetter(this, "cpmm", function() { + return Cc["@mozilla.org/childprocessmessagemanager;1"] + .getService(Ci.nsIMessageSender); +}); + +function debug(str) { + dump("=*= AlertsService.js : " + str + "\n"); +} + +// ----------------------------------------------------------------------- +// Alerts Service +// ----------------------------------------------------------------------- + +const kNotificationSystemMessageName = "notification"; + +const kMessageAlertNotificationSend = "alert-notification-send"; +const kMessageAlertNotificationClose = "alert-notification-close"; + +const kTopicAlertShow = "alertshow"; +const kTopicAlertFinished = "alertfinished"; +const kTopicAlertClickCallback = "alertclickcallback"; + +function AlertsService() { + Services.obs.addObserver(this, "xpcom-shutdown", false); +} + +AlertsService.prototype = { + classID: Components.ID("{fe33c107-82a4-41d6-8c64-5353267e04c9}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAlertsService, + Ci.nsIObserver]), + + observe: function(aSubject, aTopic, aData) { + switch (aTopic) { + case "xpcom-shutdown": + Services.obs.removeObserver(this, "xpcom-shutdown"); + break; + } + }, + + // nsIAlertsService + showAlert: function(aAlert, aAlertListener) { + if (!aAlert) { + return; + } + cpmm.sendAsyncMessage(kMessageAlertNotificationSend, { + imageURL: aAlert.imageURL, + title: aAlert.title, + text: aAlert.text, + clickable: aAlert.textClickable, + cookie: aAlert.cookie, + listener: aAlertListener, + id: aAlert.name, + dir: aAlert.dir, + lang: aAlert.lang, + dataStr: aAlert.data, + inPrivateBrowsing: aAlert.inPrivateBrowsing + }); + }, + + showAlertNotification: function(aImageUrl, aTitle, aText, aTextClickable, + aCookie, aAlertListener, aName, aBidi, + aLang, aDataStr, aPrincipal, + aInPrivateBrowsing) { + let alert = Cc["@mozilla.org/alert-notification;1"]. + createInstance(Ci.nsIAlertNotification); + + alert.init(aName, aImageUrl, aTitle, aText, aTextClickable, aCookie, + aBidi, aLang, aDataStr, aPrincipal, aInPrivateBrowsing); + + this.showAlert(alert, aAlertListener); + }, + + closeAlert: function(aName) { + cpmm.sendAsyncMessage(kMessageAlertNotificationClose, { + name: aName + }); + }, + + // AlertsService.js custom implementation + _listeners: [], + + receiveMessage: function(aMessage) { + let data = aMessage.data; + let listener = this._listeners[data.uid]; + if (!listener) { + return; + } + + let topic = data.topic; + + try { + listener.observer.observe(null, topic, null); + } catch (e) { + if (topic === kTopicAlertFinished && listener.dbId) { + notificationStorage.delete(listener.manifestURL, listener.dbId); + } + } + + // we're done with this notification + if (topic === kTopicAlertFinished) { + delete this._listeners[data.uid]; + } + }, + + deserializeStructuredClone: function(dataString) { + if (!dataString) { + return null; + } + let scContainer = Cc["@mozilla.org/docshell/structured-clone-container;1"]. + createInstance(Ci.nsIStructuredCloneContainer); + + // The maximum supported structured-clone serialization format version + // as defined in "js/public/StructuredClone.h" + let JS_STRUCTURED_CLONE_VERSION = 4; + scContainer.initFromBase64(dataString, JS_STRUCTURED_CLONE_VERSION); + let dataObj = scContainer.deserializeToVariant(); + + // We have to check whether dataObj contains DOM objects (supported by + // nsIStructuredCloneContainer, but not by Cu.cloneInto), e.g. ImageData. + // After the structured clone callback systems will be unified, we'll not + // have to perform this check anymore. + try { + let data = Cu.cloneInto(dataObj, {}); + } catch(e) { dataObj = null; } + + return dataObj; + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AlertsService]); diff --git a/b2g/components/B2GAboutRedirector.js b/b2g/components/B2GAboutRedirector.js new file mode 100644 index 000000000..f4bcf47f4 --- /dev/null +++ b/b2g/components/B2GAboutRedirector.js @@ -0,0 +1,78 @@ +/* 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 Cc = Components.classes; +const Ci = Components.interfaces; + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +function debug(msg) { + //dump("B2GAboutRedirector: " + msg + "\n"); +} + +function netErrorURL() { + let systemManifestURL = Services.prefs.getCharPref("b2g.system_manifest_url"); + systemManifestURL = Services.io.newURI(systemManifestURL, null, null); + let netErrorURL = Services.prefs.getCharPref("b2g.neterror.url"); + netErrorURL = Services.io.newURI(netErrorURL, null, systemManifestURL); + return netErrorURL.spec; +} + +var modules = { + certerror: { + uri: "chrome://b2g/content/aboutCertError.xhtml", + privileged: false, + hide: true + }, + neterror: { + uri: netErrorURL(), + privileged: false, + hide: true + } +}; + +function B2GAboutRedirector() {} +B2GAboutRedirector.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]), + classID: Components.ID("{920400b1-cf8f-4760-a9c4-441417b15134}"), + + _getModuleInfo: function (aURI) { + let moduleName = aURI.path.replace(/[?#].*/, "").toLowerCase(); + return modules[moduleName]; + }, + + // nsIAboutModule + getURIFlags: function(aURI) { + let flags; + let moduleInfo = this._getModuleInfo(aURI); + if (moduleInfo.hide) + flags = Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT; + + return flags | Ci.nsIAboutModule.ALLOW_SCRIPT; + }, + + newChannel: function(aURI, aLoadInfo) { + let moduleInfo = this._getModuleInfo(aURI); + + var ios = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + + var newURI = ios.newURI(moduleInfo.uri, null, null); + + var channel = ios.newChannelFromURIWithLoadInfo(newURI, aLoadInfo); + + if (!moduleInfo.privileged) { + // Setting the owner to null means that we'll go through the normal + // path in GetChannelPrincipal and create a codebase principal based + // on the channel's originalURI + channel.owner = null; + } + + channel.originalURI = aURI; + + return channel; + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([B2GAboutRedirector]); diff --git a/b2g/components/B2GAppMigrator.js b/b2g/components/B2GAppMigrator.js new file mode 100644 index 000000000..65671d151 --- /dev/null +++ b/b2g/components/B2GAppMigrator.js @@ -0,0 +1,152 @@ +/* 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'; + +function debug(s) { + dump("-*- B2GAppMigrator.js: " + s + "\n"); +} +const DEBUG = false; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +const kMigrationMessageName = "webapps-before-update-merge"; + +const kIDBDirType = "indexedDBPDir"; +const kProfileDirType = "ProfD"; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "appsService", + "@mozilla.org/AppsService;1", + "nsIAppsService"); + +function B2GAppMigrator() { +} + +B2GAppMigrator.prototype = { + classID: Components.ID('{7211ece0-b458-4635-9afc-f8d7f376ee95}'), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + executeBrowserMigration: function() { + if (DEBUG) debug("Executing Browser Migration"); + // The browser db file and directory names are hashed the same way + // everywhere, so it should be the same on all systems. We should + // be able to just hardcode it. + let browserDBDirName = "2959517650brreosw"; + let browserDBFileName = browserDBDirName + ".sqlite"; + + // Storage directories need to be prefixed with the local id of + // the app + let browserLocalAppId = appsService.getAppLocalIdByManifestURL("app://browser.gaiamobile.org/manifest.webapp"); + let browserAppStorageDirName = browserLocalAppId + "+f+app+++browser.gaiamobile.org"; + + // On the phone, the browser db will only be in the old IDB + // directory, since it only existed up until v2.0. On desktop, it + // will exist in the profile directory. + // + // Uses getDir with filename appending to make sure we don't + // create extra directories along the way if they don't already + // exist. + let browserDBFile = FileUtils.getDir(kIDBDirType, + ["storage", + "persistent", + browserAppStorageDirName, + "idb"], false, true); + browserDBFile.append(browserDBFileName); + let browserDBDir = FileUtils.getDir(kIDBDirType, + ["storage", + "persistent", + browserAppStorageDirName, + "idb", + browserDBDirName + ], false, true); + + if (!browserDBFile.exists()) { + if (DEBUG) debug("Browser DB " + browserDBFile.path + " does not exist, trying profile location"); + browserDBFile = FileUtils.getDir(kProfileDirType, + ["storage", + "persistent", + browserAppStorageDirName, + "idb"], false, true); + browserDBFile.append(browserDBFileName); + if (!browserDBFile.exists()) { + if (DEBUG) debug("Browser DB " + browserDBFile.path + " does not exist. Cannot copy browser db."); + return; + } + // If we have confirmed we have a DB file, we should also have a + // directory. + browserDBDir = FileUtils.getDir(kProfileDirType, + ["storage", + "persistent", + browserAppStorageDirName, + "idb", + browserDBDirName + ], false, true); + } + + let systemLocalAppId = appsService.getAppLocalIdByManifestURL("app://system.gaiamobile.org/manifest.webapp"); + let systemAppStorageDirName = systemLocalAppId + "+f+app+++system.gaiamobile.org"; + + // This check futureproofs the system DB storage directory. It + // currently exists outside of the profile but will most likely + // move into the profile at some point. + let systemDBDir = FileUtils.getDir(kIDBDirType, + ["storage", + "persistent", + systemAppStorageDirName, + "idb"], false, true); + + if (!systemDBDir.exists()) { + if (DEBUG) debug("System DB directory " + systemDBDir.path + " does not exist, trying profile location"); + systemDBDir = FileUtils.getDir(kProfileDirType, + ["storage", + "persistent", + systemAppStorageDirName, + "idb"], false, true); + if (!systemDBDir.exists()) { + if (DEBUG) debug("System DB directory " + systemDBDir.path + " does not exist. Cannot copy browser db."); + return; + } + } + + if (DEBUG) { + debug("Browser DB file exists, copying"); + debug("Browser local id: " + browserLocalAppId + ""); + debug("System local id: " + systemLocalAppId + ""); + debug("Browser DB file path: " + browserDBFile.path + ""); + debug("Browser DB dir path: " + browserDBDir.path + ""); + debug("System DB directory path: " + systemDBDir.path + ""); + } + + try { + browserDBFile.copyTo(systemDBDir, browserDBFileName); + } catch (e) { + debug("File copy caused error! " + e.name); + } + try { + browserDBDir.copyTo(systemDBDir, browserDBDirName); + } catch (e) { + debug("Dir copy caused error! " + e.name); + } + if (DEBUG) debug("Browser DB copied successfully"); + }, + + observe: function(subject, topic, data) { + switch (topic) { + case kMigrationMessageName: + this.executeBrowserMigration(); + break; + default: + debug("Unhandled topic: " + topic); + break; + } + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([B2GAppMigrator]); diff --git a/b2g/components/B2GComponents.manifest b/b2g/components/B2GComponents.manifest new file mode 100644 index 000000000..53d0032f9 --- /dev/null +++ b/b2g/components/B2GComponents.manifest @@ -0,0 +1,108 @@ +# Scrollbars +category agent-style-sheets browser-content-stylesheet chrome://b2g/content/content.css + +# AlertsService.js +component {fe33c107-82a4-41d6-8c64-5353267e04c9} AlertsService.js +contract @mozilla.org/system-alerts-service;1 {fe33c107-82a4-41d6-8c64-5353267e04c9} + +# ContentPermissionPrompt.js +component {8c719f03-afe0-4aac-91ff-6c215895d467} ContentPermissionPrompt.js +contract @mozilla.org/content-permission/prompt;1 {8c719f03-afe0-4aac-91ff-6c215895d467} + +#ifdef MOZ_UPDATER +# UpdatePrompt.js +component {88b3eb21-d072-4e3b-886d-f89d8c49fe59} UpdatePrompt.js +contract @mozilla.org/updates/update-prompt;1 {88b3eb21-d072-4e3b-886d-f89d8c49fe59} +category system-update-provider MozillaProvider @mozilla.org/updates/update-prompt;1,{88b3eb21-d072-4e3b-886d-f89d8c49fe59} +#endif + +#ifdef MOZ_B2G +# DirectoryProvider.js +component {9181eb7c-6f87-11e1-90b1-4f59d80dd2e5} DirectoryProvider.js +contract @mozilla.org/b2g/directory-provider;1 {9181eb7c-6f87-11e1-90b1-4f59d80dd2e5} +category xpcom-directory-providers b2g-directory-provider @mozilla.org/b2g/directory-provider;1 +#endif + +# SystemMessageGlue.js +component {2846f034-e614-11e3-93cd-74d02b97e723} SystemMessageGlue.js +contract @mozilla.org/dom/messages/system-message-glue;1 {2846f034-e614-11e3-93cd-74d02b97e723} + +# ProcessGlobal.js +component {1a94c87a-5ece-4d11-91e1-d29c29f21b28} ProcessGlobal.js +contract @mozilla.org/b2g-process-global;1 {1a94c87a-5ece-4d11-91e1-d29c29f21b28} +category app-startup ProcessGlobal service,@mozilla.org/b2g-process-global;1 + +# OMAContentHandler.js +component {a6b2ab13-9037-423a-9897-dde1081be323} OMAContentHandler.js +contract @mozilla.org/uriloader/content-handler;1?type=application/vnd.oma.drm.message {a6b2ab13-9037-423a-9897-dde1081be323} +contract @mozilla.org/uriloader/content-handler;1?type=application/vnd.oma.dd+xml {a6b2ab13-9037-423a-9897-dde1081be323} + +# TelProtocolHandler.js +component {782775dd-7351-45ea-aff1-0ffa872cfdd2} TelProtocolHandler.js +contract @mozilla.org/network/protocol;1?name=tel {782775dd-7351-45ea-aff1-0ffa872cfdd2} + +# SmsProtocolHandler.js +component {81ca20cb-0dad-4e32-8566-979c8998bd73} SmsProtocolHandler.js +contract @mozilla.org/network/protocol;1?name=sms {81ca20cb-0dad-4e32-8566-979c8998bd73} + +# MailtoProtocolHandler.js +component {50777e53-0331-4366-a191-900999be386c} MailtoProtocolHandler.js +contract @mozilla.org/network/protocol;1?name=mailto {50777e53-0331-4366-a191-900999be386c} + +# RecoveryService.js +component {b3caca5d-0bb0-48c6-912b-6be6cbf08832} RecoveryService.js +contract @mozilla.org/recovery-service;1 {b3caca5d-0bb0-48c6-912b-6be6cbf08832} + +# B2GAboutRedirector +component {920400b1-cf8f-4760-a9c4-441417b15134} B2GAboutRedirector.js +contract @mozilla.org/network/protocol/about;1?what=certerror {920400b1-cf8f-4760-a9c4-441417b15134} +contract @mozilla.org/network/protocol/about;1?what=neterror {920400b1-cf8f-4760-a9c4-441417b15134} + +#ifndef MOZ_GRAPHENE +# FilePicker.js +component {436ff8f9-0acc-4b11-8ec7-e293efba3141} FilePicker.js +contract @mozilla.org/filepicker;1 {436ff8f9-0acc-4b11-8ec7-e293efba3141} +#endif + +# FxAccountsUIGlue.js +component {51875c14-91d7-4b8c-b65d-3549e101228c} FxAccountsUIGlue.js +contract @mozilla.org/fxaccounts/fxaccounts-ui-glue;1 {51875c14-91d7-4b8c-b65d-3549e101228c} + +# HelperAppDialog.js +component {710322af-e6ae-4b0c-b2c9-1474a87b077e} HelperAppDialog.js +contract @mozilla.org/helperapplauncherdialog;1 {710322af-e6ae-4b0c-b2c9-1474a87b077e} + +#ifndef MOZ_WIDGET_GONK +component {c83c02c0-5d43-4e3e-987f-9173b313e880} SimulatorScreen.js +contract @mozilla.org/simulator-screen;1 {c83c02c0-5d43-4e3e-987f-9173b313e880} +category profile-after-change SimulatorScreen @mozilla.org/simulator-screen;1 + +component {e30b0e13-2d12-4cb0-bc4c-4e617a1bf76e} OopCommandLine.js +contract @mozilla.org/commandlinehandler/general-startup;1?type=b2goop {e30b0e13-2d12-4cb0-bc4c-4e617a1bf76e} +category command-line-handler m-b2goop @mozilla.org/commandlinehandler/general-startup;1?type=b2goop + +component {385993fe-8710-4621-9fb1-00a09d8bec37} CommandLine.js +contract @mozilla.org/commandlinehandler/general-startup;1?type=b2gcmds {385993fe-8710-4621-9fb1-00a09d8bec37} +category command-line-handler m-b2gcmds @mozilla.org/commandlinehandler/general-startup;1?type=b2gcmds +#endif + +# BootstrapCommandLine.js +component {fd663ec8-cf3f-4c2b-aacb-17a6915ccb44} BootstrapCommandLine.js +contract @mozilla.org/commandlinehandler/general-startup;1?type=b2gbootstrap {fd663ec8-cf3f-4c2b-aacb-17a6915ccb44} +category command-line-handler m-b2gbootstrap @mozilla.org/commandlinehandler/general-startup;1?type=b2gbootstrap + +# B2GAppMigrator.js +component {7211ece0-b458-4635-9afc-f8d7f376ee95} B2GAppMigrator.js +contract @mozilla.org/app-migrator;1 {7211ece0-b458-4635-9afc-f8d7f376ee95} + +# B2GPresentationDevicePrompt.js +component {4a300c26-e99b-4018-ab9b-c48cf9bc4de1} B2GPresentationDevicePrompt.js +contract @mozilla.org/presentation-device/prompt;1 {4a300c26-e99b-4018-ab9b-c48cf9bc4de1} + +# PresentationRequestUIGlue.js +component {ccc8a839-0b64-422b-8a60-fb2af0e376d0} PresentationRequestUIGlue.js +contract @mozilla.org/presentation/requestuiglue;1 {ccc8a839-0b64-422b-8a60-fb2af0e376d0} + +# SystemMessageInternal.js +component {70589ca5-91ac-4b9e-b839-d6a88167d714} SystemMessageInternal.js +contract @mozilla.org/system-message-internal;1 {70589ca5-91ac-4b9e-b839-d6a88167d714} diff --git a/b2g/components/B2GPresentationDevicePrompt.js b/b2g/components/B2GPresentationDevicePrompt.js new file mode 100644 index 000000000..998e0b7ac --- /dev/null +++ b/b2g/components/B2GPresentationDevicePrompt.js @@ -0,0 +1,87 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function debug(aMsg) { + //dump("-*- B2GPresentationDevicePrompt: " + aMsg + "\n"); +} + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +const kB2GPRESENTATIONDEVICEPROMPT_CONTRACTID = "@mozilla.org/presentation-device/prompt;1"; +const kB2GPRESENTATIONDEVICEPROMPT_CID = Components.ID("{4a300c26-e99b-4018-ab9b-c48cf9bc4de1}"); + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy", + "resource://gre/modules/SystemAppProxy.jsm"); + +function B2GPresentationDevicePrompt() {} + +B2GPresentationDevicePrompt.prototype = { + classID: kB2GPRESENTATIONDEVICEPROMPT_CID, + contractID: kB2GPRESENTATIONDEVICEPROMPT_CONTRACTID, + classDescription: "B2G Presentation Device Prompt", + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevicePrompt]), + + // nsIPresentationDevicePrompt + promptDeviceSelection: function(aRequest) { + let self = this; + let requestId = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator).generateUUID().toString(); + + SystemAppProxy.addEventListener("mozContentEvent", function contentEvent(aEvent) { + let detail = aEvent.detail; + if (detail.id !== requestId) { + return; + } + + SystemAppProxy.removeEventListener("mozContentEvent", contentEvent); + + switch (detail.type) { + case "presentation-select-result": + debug("device " + detail.deviceId + " is selected by user"); + let device = self._getDeviceById(detail.deviceId); + if (!device) { + debug("cancel request because device is not found"); + aRequest.cancel(Cr.NS_ERROR_DOM_NOT_FOUND_ERR); + } + aRequest.select(device); + break; + case "presentation-select-deny": + debug("request canceled by user"); + aRequest.cancel(Cr.NS_ERROR_DOM_NOT_ALLOWED_ERR); + break; + } + }); + + let detail = { + type: "presentation-select-device", + origin: aRequest.origin, + requestURL: aRequest.requestURL, + id: requestId, + }; + + SystemAppProxy.dispatchEvent(detail); + }, + + _getDeviceById: function(aDeviceId) { + let deviceManager = Cc["@mozilla.org/presentation-device/manager;1"] + .getService(Ci.nsIPresentationDeviceManager); + let devices = deviceManager.getAvailableDevices().QueryInterface(Ci.nsIArray); + + for (let i = 0; i < devices.length; i++) { + let device = devices.queryElementAt(i, Ci.nsIPresentationDevice); + if (device.id === aDeviceId) { + return device; + } + } + + return null; + }, +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([B2GPresentationDevicePrompt]); diff --git a/b2g/components/BootstrapCommandLine.js b/b2g/components/BootstrapCommandLine.js new file mode 100644 index 000000000..24d9f5461 --- /dev/null +++ b/b2g/components/BootstrapCommandLine.js @@ -0,0 +1,52 @@ +/* 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/AppsUtils.jsm"); + +function BootstrapCommandlineHandler() { + this.wrappedJSObject = this; + this.startManifestURL = null; +} + +BootstrapCommandlineHandler.prototype = { + bailout: function(aMsg) { + dump("************************************************************\n"); + dump("* /!\\ " + aMsg + "\n"); + dump("************************************************************\n"); + let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"] + .getService(Ci.nsIAppStartup); + appStartup.quit(appStartup.eForceQuit); + }, + + handle: function(aCmdLine) { + this.startManifestURL = null; + + try { + // Returns null if the argument was not specified. Throws + // NS_ERROR_INVALID_ARG if there is no parameter specified (because + // it was the last argument or the next argument starts with '-'). + // However, someone could still explicitly pass an empty argument! + this.startManifestURL = aCmdLine.handleFlagWithParam("start-manifest", false); + } catch(e) { + return; + } + + if (!this.startManifestURL) { + return; + } + + if (!isAbsoluteURI(this.startManifestURL)) { + this.bailout("The start manifest url must be absolute."); + return; + } + }, + + helpInfo: "--start-manifest=manifest_url", + classID: Components.ID("{fd663ec8-cf3f-4c2b-aacb-17a6915ccb44}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsICommandLineHandler]) +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([BootstrapCommandlineHandler]); diff --git a/b2g/components/Bootstraper.jsm b/b2g/components/Bootstraper.jsm new file mode 100644 index 000000000..3d3fb37d9 --- /dev/null +++ b/b2g/components/Bootstraper.jsm @@ -0,0 +1,156 @@ +/* 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 = ["Bootstraper"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const CC = Components.Constructor; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/AppsUtils.jsm"); + +function debug(aMsg) { + //dump("-*- Bootstraper: " + aMsg + "\n"); +} + +/** + * This module loads the manifest for app from the --start-url enpoint and + * ensures that it's installed as the system app. + */ +this.Bootstraper = { + _manifestURL: null, + _startupURL: null, + + bailout: function(aMsg) { + dump("************************************************************\n"); + dump("* /!\\ " + aMsg + "\n"); + dump("************************************************************\n"); + let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"] + .getService(Ci.nsIAppStartup); + appStartup.quit(appStartup.eForceQuit); + }, + + installSystemApp: function(aManifest) { + // Get the appropriate startup url from the manifest launch_path. + let base = Services.io.newURI(this._manifestURL, null, null); + let origin = base.prePath; + let helper = new ManifestHelper(aManifest, origin, this._manifestURL); + this._startupURL = helper.fullLaunchPath(); + + return new Promise((aResolve, aReject) => { + debug("Origin is " + origin); + let appData = { + app: { + installOrigin: origin, + origin: origin, + manifest: aManifest, + manifestURL: this._manifestURL, + manifestHash: AppsUtils.computeHash(JSON.stringify(aManifest)), + appStatus: Ci.nsIPrincipal.APP_STATUS_CERTIFIED + }, + appId: 1, + isBrowser: false, + isPackage: false + }; + + //DOMApplicationRegistry.confirmInstall(appData, null, aResolve); + }); + }, + + /** + * Resolves to a json manifest. + */ + loadManifest: function() { + return new Promise((aResolve, aReject) => { + debug("Loading manifest " + this._manifestURL); + + let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + xhr.mozBackgroundRequest = true; + xhr.open("GET", this._manifestURL); + xhr.responseType = "json"; + xhr.addEventListener("load", () => { + if (xhr.status >= 200 && xhr.status < 400) { + debug("Success loading " + this._manifestURL); + aResolve(xhr.response); + } else { + aReject("Error loading " + this._manifestURL); + } + }); + xhr.addEventListener("error", () => { + aReject("Error loading " + this._manifestURL); + }); + xhr.send(null); + }); + }, + + configure: function() { + debug("Setting startup prefs... " + this._startupURL); + Services.prefs.setCharPref("b2g.system_manifest_url", this._manifestURL); + Services.prefs.setCharPref("b2g.system_startup_url", this._startupURL); + return Promise.resolve(); + }, + + /** + * If a system app is already installed, uninstall it so that we can + * cleanly replace it by the current one. + */ + uninstallPreviousSystemApp: function() { + // TODO: FIXME + return Promise.resolve(); + + let oldManifestURL; + try{ + oldManifestURL = Services.prefs.getCharPref("b2g.system_manifest_url"); + } catch(e) { + // No preference set, so nothing to uninstall. + return Promise.resolve(); + } + + let id = DOMApplicationRegistry.getAppLocalIdByManifestURL(oldManifestURL); + if (id == Ci.nsIScriptSecurityManager.NO_APP_ID) { + return Promise.resolve(); + } + debug("Uninstalling " + oldManifestURL); + return DOMApplicationRegistry.uninstall(oldManifestURL); + }, + + /** + * Check if we are already configured to run from this manifest url. + */ + isInstallRequired: function(aManifestURL) { + try { + if (Services.prefs.getCharPref("b2g.system_manifest_url") == aManifestURL) { + return false; + } + } catch(e) { } + return true; + }, + + /** + * Resolves once we have installed the app. + */ + ensureSystemAppInstall: function(aManifestURL) { + this._manifestURL = aManifestURL; + debug("Installing app from " + this._manifestURL); + + if (!this.isInstallRequired(this._manifestURL)) { + debug("Already configured for " + this._manifestURL); + return Promise.resolve(); + } + + return new Promise((aResolve, aReject) => { + this.uninstallPreviousSystemApp.bind(this) + .then(this.loadManifest.bind(this)) + .then(this.installSystemApp.bind(this)) + .then(this.configure.bind(this)) + .then(aResolve) + .catch(aReject); + }); + } +}; diff --git a/b2g/components/CommandLine.js b/b2g/components/CommandLine.js new file mode 100644 index 000000000..6dc48bd33 --- /dev/null +++ b/b2g/components/CommandLine.js @@ -0,0 +1,29 @@ +/* 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); + +// Small helper to expose nsICommandLine object to chrome code + +function CommandlineHandler() { + this.wrappedJSObject = this; +} + +CommandlineHandler.prototype = { + handle: function(cmdLine) { + this.cmdLine = cmdLine; + let win = Services.wm.getMostRecentWindow("navigator:browser"); + if (win && win.shell) { + win.shell.handleCmdLine(); + } + }, + + helpInfo: "", + classID: Components.ID("{385993fe-8710-4621-9fb1-00a09d8bec37}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsICommandLineHandler]), +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([CommandlineHandler]); diff --git a/b2g/components/ContentPermissionPrompt.js b/b2g/components/ContentPermissionPrompt.js new file mode 100644 index 000000000..e11b1b458 --- /dev/null +++ b/b2g/components/ContentPermissionPrompt.js @@ -0,0 +1,461 @@ +/* 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" + +function debug(str) { + //dump("-*- ContentPermissionPrompt: " + str + "\n"); +} + +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; +const Cc = Components.classes; + +const PROMPT_FOR_UNKNOWN = ["audio-capture", + "desktop-notification", + "geolocation", + "video-capture"]; +// Due to privary issue, permission requests like GetUserMedia should prompt +// every time instead of providing session persistence. +const PERMISSION_NO_SESSION = ["audio-capture", "video-capture"]; +const ALLOW_MULTIPLE_REQUESTS = ["audio-capture", "video-capture"]; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/AppsUtils.jsm"); +Cu.import("resource://gre/modules/PermissionsInstaller.jsm"); +Cu.import("resource://gre/modules/PermissionsTable.jsm"); + +var permissionManager = Cc["@mozilla.org/permissionmanager;1"].getService(Ci.nsIPermissionManager); +var secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(Ci.nsIScriptSecurityManager); + +var permissionSpecificChecker = {}; + +XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy", + "resource://gre/modules/SystemAppProxy.jsm"); + +/** + * Determine if a permission should be prompt to user or not. + * + * @param aPerm requested permission + * @param aAction the action according to principal + * @return true if prompt is required + */ +function shouldPrompt(aPerm, aAction) { + return ((aAction == Ci.nsIPermissionManager.PROMPT_ACTION) || + (aAction == Ci.nsIPermissionManager.UNKNOWN_ACTION && + PROMPT_FOR_UNKNOWN.indexOf(aPerm) >= 0)); +} + +/** + * Create the default choices for the requested permissions + * + * @param aTypesInfo requested permissions + * @return the default choices for permissions with options, return + * undefined if no option in all requested permissions. + */ +function buildDefaultChoices(aTypesInfo) { + let choices; + for (let type of aTypesInfo) { + if (type.options.length > 0) { + if (!choices) { + choices = {}; + } + choices[type.access] = type.options[0]; + } + } + return choices; +} + +/** + * aTypesInfo is an array of {permission, access, action, deny} which keeps + * the information of each permission. This arrary is initialized in + * ContentPermissionPrompt.prompt and used among functions. + * + * aTypesInfo[].permission : permission name + * aTypesInfo[].access : permission name + request.access + * aTypesInfo[].action : the default action of this permission + * aTypesInfo[].deny : true if security manager denied this app's origin + * principal. + * Note: + * aTypesInfo[].permission will be sent to prompt only when + * aTypesInfo[].action is PROMPT_ACTION and aTypesInfo[].deny is false. + */ +function rememberPermission(aTypesInfo, aPrincipal, aSession) +{ + function convertPermToAllow(aPerm, aPrincipal) + { + let type = + permissionManager.testExactPermissionFromPrincipal(aPrincipal, aPerm); + if (shouldPrompt(aPerm, type)) { + debug("add " + aPerm + " to permission manager with ALLOW_ACTION"); + if (!aSession) { + permissionManager.addFromPrincipal(aPrincipal, + aPerm, + Ci.nsIPermissionManager.ALLOW_ACTION); + } else if (PERMISSION_NO_SESSION.indexOf(aPerm) < 0) { + permissionManager.addFromPrincipal(aPrincipal, + aPerm, + Ci.nsIPermissionManager.ALLOW_ACTION, + Ci.nsIPermissionManager.EXPIRE_SESSION, 0); + } + } + } + + for (let i in aTypesInfo) { + // Expand the permission to see if we have multiple access properties + // to convert + let perm = aTypesInfo[i].permission; + let access = PermissionsTable[perm].access; + if (access) { + for (let idx in access) { + convertPermToAllow(perm + "-" + access[idx], aPrincipal); + } + } else { + convertPermToAllow(perm, aPrincipal); + } + } +} + +function ContentPermissionPrompt() {} + +ContentPermissionPrompt.prototype = { + + handleExistingPermission: function handleExistingPermission(request, + typesInfo) { + typesInfo.forEach(function(type) { + type.action = + Services.perms.testExactPermissionFromPrincipal(request.principal, + type.access); + if (shouldPrompt(type.access, type.action)) { + type.action = Ci.nsIPermissionManager.PROMPT_ACTION; + } + }); + + // If all permissions are allowed already and no more than one option, + // call allow() without prompting. + let checkAllowPermission = function(type) { + if (type.action == Ci.nsIPermissionManager.ALLOW_ACTION && + type.options.length <= 1) { + return true; + } + return false; + } + if (typesInfo.every(checkAllowPermission)) { + debug("all permission requests are allowed"); + request.allow(buildDefaultChoices(typesInfo)); + return true; + } + + // If all permissions are DENY_ACTION or UNKNOWN_ACTION, call cancel() + // without prompting. + let checkDenyPermission = function(type) { + if (type.action == Ci.nsIPermissionManager.DENY_ACTION || + type.action == Ci.nsIPermissionManager.UNKNOWN_ACTION) { + return true; + } + return false; + } + if (typesInfo.every(checkDenyPermission)) { + debug("all permission requests are denied"); + request.cancel(); + return true; + } + return false; + }, + + // multiple requests should be audio and video + checkMultipleRequest: function checkMultipleRequest(typesInfo) { + if (typesInfo.length == 1) { + return true; + } else if (typesInfo.length > 1) { + let checkIfAllowMultiRequest = function(type) { + return (ALLOW_MULTIPLE_REQUESTS.indexOf(type.access) !== -1); + } + if (typesInfo.every(checkIfAllowMultiRequest)) { + debug("legal multiple requests"); + return true; + } + } + + return false; + }, + + handledByApp: function handledByApp(request, typesInfo) { + if (request.principal.appId == Ci.nsIScriptSecurityManager.NO_APP_ID || + request.principal.appId == Ci.nsIScriptSecurityManager.UNKNOWN_APP_ID) { + // This should not really happen + request.cancel(); + return true; + } + + let appsService = Cc["@mozilla.org/AppsService;1"] + .getService(Ci.nsIAppsService); + let app = appsService.getAppByLocalId(request.principal.appId); + + // Check each permission if it's denied by permission manager with app's + // URL. + let notDenyAppPrincipal = function(type) { + let url = Services.io.newURI(app.origin, null, null); + let principal = + secMan.createCodebasePrincipal(url, + {appId: request.principal.appId}); + let result = Services.perms.testExactPermissionFromPrincipal(principal, + type.access); + + if (result == Ci.nsIPermissionManager.ALLOW_ACTION || + result == Ci.nsIPermissionManager.PROMPT_ACTION) { + type.deny = false; + } + return !type.deny; + } + // Cancel the entire request if one of the requested permissions is denied + if (!typesInfo.every(notDenyAppPrincipal)) { + request.cancel(); + return true; + } + + return false; + }, + + handledByPermissionType: function handledByPermissionType(request, typesInfo) { + for (let i in typesInfo) { + if (permissionSpecificChecker.hasOwnProperty(typesInfo[i].permission) && + permissionSpecificChecker[typesInfo[i].permission](request)) { + return true; + } + } + + return false; + }, + + prompt: function(request) { + // Initialize the typesInfo and set the default value. + let typesInfo = []; + let perms = request.types.QueryInterface(Ci.nsIArray); + for (let idx = 0; idx < perms.length; idx++) { + let perm = perms.queryElementAt(idx, Ci.nsIContentPermissionType); + let tmp = { + permission: perm.type, + access: (perm.access && perm.access !== "unused") ? + perm.type + "-" + perm.access : perm.type, + options: [], + deny: true, + action: Ci.nsIPermissionManager.UNKNOWN_ACTION + }; + + // Append available options, if any. + let options = perm.options.QueryInterface(Ci.nsIArray); + for (let i = 0; i < options.length; i++) { + let option = options.queryElementAt(i, Ci.nsISupportsString).data; + tmp.options.push(option); + } + typesInfo.push(tmp); + } + + if (secMan.isSystemPrincipal(request.principal)) { + request.allow(buildDefaultChoices(typesInfo)); + return; + } + + + if (typesInfo.length == 0) { + request.cancel(); + return; + } + + if(!this.checkMultipleRequest(typesInfo)) { + request.cancel(); + return; + } + + if (this.handledByApp(request, typesInfo) || + this.handledByPermissionType(request, typesInfo)) { + return; + } + + // returns true if the request was handled + if (this.handleExistingPermission(request, typesInfo)) { + return; + } + + // prompt PROMPT_ACTION request or request with options. + typesInfo = typesInfo.filter(function(type) { + return !type.deny && (type.action == Ci.nsIPermissionManager.PROMPT_ACTION || type.options.length > 0) ; + }); + + if (!request.element) { + this.delegatePrompt(request, typesInfo); + return; + } + + var cancelRequest = function() { + request.requester.onVisibilityChange = null; + request.cancel(); + } + + var self = this; + + // If the request was initiated from a hidden iframe + // we don't forward it to content and cancel it right away + request.requester.getVisibility( { + notifyVisibility: function(isVisible) { + if (!isVisible) { + cancelRequest(); + return; + } + + // Monitor the frame visibility and cancel the request if the frame goes + // away but the request is still here. + request.requester.onVisibilityChange = { + notifyVisibility: function(isVisible) { + if (isVisible) + return; + + self.cancelPrompt(request, typesInfo); + cancelRequest(); + } + } + + self.delegatePrompt(request, typesInfo, function onCallback() { + request.requester.onVisibilityChange = null; + }); + } + }); + + }, + + cancelPrompt: function(request, typesInfo) { + this.sendToBrowserWindow("cancel-permission-prompt", request, + typesInfo); + }, + + delegatePrompt: function(request, typesInfo, callback) { + this.sendToBrowserWindow("permission-prompt", request, typesInfo, + function(type, remember, choices) { + if (type == "permission-allow") { + rememberPermission(typesInfo, request.principal, !remember); + if (callback) { + callback(); + } + request.allow(choices); + return; + } + + let addDenyPermission = function(type) { + debug("add " + type.permission + + " to permission manager with DENY_ACTION"); + if (remember) { + Services.perms.addFromPrincipal(request.principal, type.access, + Ci.nsIPermissionManager.DENY_ACTION); + } else if (PERMISSION_NO_SESSION.indexOf(type.access) < 0) { + Services.perms.addFromPrincipal(request.principal, type.access, + Ci.nsIPermissionManager.DENY_ACTION, + Ci.nsIPermissionManager.EXPIRE_SESSION, + 0); + } + } + try { + // This will trow if we are canceling because the remote process died. + // Just eat the exception and call the callback that will cleanup the + // visibility event listener. + typesInfo.forEach(addDenyPermission); + } catch(e) { } + + if (callback) { + callback(); + } + + try { + request.cancel(); + } catch(e) { } + }); + }, + + sendToBrowserWindow: function(type, request, typesInfo, callback) { + let requestId = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator).generateUUID().toString(); + if (callback) { + SystemAppProxy.addEventListener("mozContentEvent", function contentEvent(evt) { + let detail = evt.detail; + if (detail.id != requestId) + return; + SystemAppProxy.removeEventListener("mozContentEvent", contentEvent); + + callback(detail.type, detail.remember, detail.choices); + }) + } + + let principal = request.principal; + let isApp = principal.appStatus != Ci.nsIPrincipal.APP_STATUS_NOT_INSTALLED; + let remember = (principal.appStatus == Ci.nsIPrincipal.APP_STATUS_PRIVILEGED || + principal.appStatus == Ci.nsIPrincipal.APP_STATUS_CERTIFIED) + ? true + : request.remember; + let isGranted = typesInfo.every(function(type) { + return type.action == Ci.nsIPermissionManager.ALLOW_ACTION; + }); + let permissions = {}; + for (let i in typesInfo) { + debug("prompt " + typesInfo[i].permission); + permissions[typesInfo[i].permission] = typesInfo[i].options; + } + + let details = { + type: type, + permissions: permissions, + id: requestId, + // This system app uses the origin from permission events to + // compare against the mozApp.origin of app windows, so we + // are not concerned with origin suffixes here (appId, etc). + origin: principal.originNoSuffix, + isApp: isApp, + remember: remember, + isGranted: isGranted, + }; + + if (isApp) { + details.manifestURL = DOMApplicationRegistry.getManifestURLByLocalId(principal.appId); + } + + // request.element is defined for OOP content, while request.window + // is defined for In-Process content. + // In both cases the message needs to be dispatched to the top-level + // <iframe mozbrowser> container in the system app. + // So the above code iterates over window.realFrameElement in order + // to crosss mozbrowser iframes boundaries and find the top-level + // one in the system app. + // window.realFrameElement will be |null| if the code try to cross + // content -> chrome boundaries. + let targetElement = request.element; + let targetWindow = request.window || targetElement.ownerDocument.defaultView; + while (targetWindow.realFrameElement) { + targetElement = targetWindow.realFrameElement; + targetWindow = targetElement.ownerDocument.defaultView; + } + + SystemAppProxy.dispatchEvent(details, targetElement); + }, + + classID: Components.ID("{8c719f03-afe0-4aac-91ff-6c215895d467}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt]) +}; + +(function() { + // Do not allow GetUserMedia while in call. + permissionSpecificChecker["audio-capture"] = function(request) { + let forbid = false; + + if (forbid) { + request.cancel(); + } + + return forbid; + }; +})(); + +//module initialization +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentPermissionPrompt]); diff --git a/b2g/components/ContentRequestHelper.jsm b/b2g/components/ContentRequestHelper.jsm new file mode 100644 index 000000000..14d8d250b --- /dev/null +++ b/b2g/components/ContentRequestHelper.jsm @@ -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; + } +}; diff --git a/b2g/components/DebuggerActors.js b/b2g/components/DebuggerActors.js new file mode 100644 index 000000000..318c46e68 --- /dev/null +++ b/b2g/components/DebuggerActors.js @@ -0,0 +1,83 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* 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 { Cu } = require("chrome"); +const DevToolsUtils = require("devtools/shared/DevToolsUtils"); +const promise = require("promise"); +const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm"); +const { BrowserTabList } = require("devtools/server/actors/webbrowser"); + +XPCOMUtils.defineLazyGetter(this, "Frames", function() { + const { Frames } = + Cu.import("resource://gre/modules/Frames.jsm", {}); + return Frames; +}); + +/** + * Unlike the original BrowserTabList which iterates over XUL windows, we + * override many portions to refer to Frames for the info needed here. + */ +function B2GTabList(connection) { + BrowserTabList.call(this, connection); + this._listening = false; +} + +B2GTabList.prototype = Object.create(BrowserTabList.prototype); + +B2GTabList.prototype._getBrowsers = function() { + return Frames.list().filter(frame => { + // Ignore app frames + return !frame.getAttribute("mozapp"); + }); +}; + +B2GTabList.prototype._getSelectedBrowser = function() { + return this._getBrowsers().find(frame => { + // Find the one visible browser (if any) + return !frame.classList.contains("hidden"); + }); +}; + +B2GTabList.prototype._checkListening = function() { + // The conditions from BrowserTabList are merged here, since we must listen to + // all events with our observer design. + this._listenForEventsIf(this._onListChanged && this._mustNotify || + this._actorByBrowser.size > 0); +}; + +B2GTabList.prototype._listenForEventsIf = function(shouldListen) { + if (this._listening != shouldListen) { + let op = shouldListen ? "addObserver" : "removeObserver"; + Frames[op](this); + this._listening = shouldListen; + } +}; + +B2GTabList.prototype.onFrameCreated = function(frame) { + let mozapp = frame.getAttribute("mozapp"); + if (mozapp) { + // Ignore app frames + return; + } + this._notifyListChanged(); + this._checkListening(); +}; + +B2GTabList.prototype.onFrameDestroyed = function(frame) { + let mozapp = frame.getAttribute("mozapp"); + if (mozapp) { + // Ignore app frames + return; + } + let actor = this._actorByBrowser.get(frame); + if (actor) { + this._handleActorClose(actor, frame); + } +}; + +exports.B2GTabList = B2GTabList; diff --git a/b2g/components/DirectoryProvider.js b/b2g/components/DirectoryProvider.js new file mode 100644 index 000000000..a7dccd0c9 --- /dev/null +++ b/b2g/components/DirectoryProvider.js @@ -0,0 +1,295 @@ +/* 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 Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/AppConstants.jsm"); + +const XRE_OS_UPDATE_APPLY_TO_DIR = "OSUpdApplyToD" +const UPDATE_ARCHIVE_DIR = "UpdArchD" +const LOCAL_DIR = "/data/local"; +const UPDATES_DIR = "updates/0"; +const FOTA_DIR = "updates/fota"; +const COREAPPSDIR_PREF = "b2g.coreappsdir" + +XPCOMUtils.defineLazyServiceGetter(Services, "env", + "@mozilla.org/process/environment;1", + "nsIEnvironment"); + +XPCOMUtils.defineLazyServiceGetter(Services, "um", + "@mozilla.org/updates/update-manager;1", + "nsIUpdateManager"); + +XPCOMUtils.defineLazyServiceGetter(Services, "volumeService", + "@mozilla.org/telephony/volume-service;1", + "nsIVolumeService"); + +XPCOMUtils.defineLazyServiceGetter(this, "cpmm", + "@mozilla.org/childprocessmessagemanager;1", + "nsISyncMessageSender"); + +XPCOMUtils.defineLazyGetter(this, "gExtStorage", function dp_gExtStorage() { + return Services.env.get("EXTERNAL_STORAGE"); +}); + +// This exists to mark the affected code for bug 828858. +const gUseSDCard = true; + +const VERBOSE = 1; +var log = + VERBOSE ? + function log_dump(msg) { dump("DirectoryProvider: " + msg + "\n"); } : + function log_noop(msg) { }; + +function DirectoryProvider() { +} + +DirectoryProvider.prototype = { + classID: Components.ID("{9181eb7c-6f87-11e1-90b1-4f59d80dd2e5}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider]), + _xpcom_factory: XPCOMUtils.generateSingletonFactory(DirectoryProvider), + + _profD: null, + + getFile: function(prop, persistent) { + if (AppConstants.platform === "gonk") { + return this.getFileOnGonk(prop, persistent); + } + return this.getFileNotGonk(prop, persistent); + }, + + getFileOnGonk: function(prop, persistent) { + let localProps = ["cachePDir", "webappsDir", "PrefD", "indexedDBPDir", + "permissionDBPDir", "UpdRootD"]; + if (localProps.indexOf(prop) != -1) { + let file = Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsILocalFile) + file.initWithPath(LOCAL_DIR); + persistent.value = true; + return file; + } + if (prop == "ProfD") { + let dir = Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsILocalFile); + dir.initWithPath(LOCAL_DIR+"/tests/profile"); + if (dir.exists()) { + persistent.value = true; + return dir; + } + } + if (prop == "coreAppsDir") { + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile) + file.initWithPath("/system/b2g"); + persistent.value = true; + return file; + } + if (prop == UPDATE_ARCHIVE_DIR) { + // getUpdateDir will set persistent to false since it may toggle between + // /data/local/ and /mnt/sdcard based on free space and/or availability + // of the sdcard. + // before download, check if free space is 2.1 times of update.mar + return this.getUpdateDir(persistent, UPDATES_DIR, 2.1); + } + if (prop == XRE_OS_UPDATE_APPLY_TO_DIR) { + // getUpdateDir will set persistent to false since it may toggle between + // /data/local/ and /mnt/sdcard based on free space and/or availability + // of the sdcard. + // before apply, check if free space is 1.1 times of update.mar + return this.getUpdateDir(persistent, FOTA_DIR, 1.1); + } + return null; + }, + + getFileNotGonk: function(prop, persistent) { + // In desktop builds, coreAppsDir is the same as the profile + // directory unless otherwise specified. We just need to get the + // path from the parent, and it is then used to build + // jar:remoteopenfile:// uris. + if (prop == "coreAppsDir") { + let coreAppsDirPref; + try { + coreAppsDirPref = Services.prefs.getCharPref(COREAPPSDIR_PREF); + } catch (e) { + // coreAppsDirPref may not exist if we're on an older version + // of gaia, so just fail silently. + } + let appsDir; + // If pref doesn't exist or isn't set, default to old value + if (!coreAppsDirPref || coreAppsDirPref == "") { + appsDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + appsDir.append("webapps"); + } else { + appsDir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile) + appsDir.initWithPath(coreAppsDirPref); + } + persistent.value = true; + return appsDir; + } else if (prop == "ProfD") { + let inParent = Cc["@mozilla.org/xre/app-info;1"] + .getService(Ci.nsIXULRuntime) + .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; + if (inParent) { + // Just bail out to use the default from toolkit. + return null; + } + if (!this._profD) { + this._profD = cpmm.sendSyncMessage("getProfD", {})[0]; + } + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(this._profD); + persistent.value = true; + return file; + } + return null; + }, + + // The VolumeService only exists on the device, and not on desktop + volumeHasFreeSpace: function dp_volumeHasFreeSpace(volumePath, requiredSpace) { + if (!volumePath) { + return false; + } + if (!Services.volumeService) { + return false; + } + let volume = Services.volumeService.createOrGetVolumeByPath(volumePath); + if (!volume || volume.state !== Ci.nsIVolume.STATE_MOUNTED) { + return false; + } + let stat = volume.getStats(); + if (!stat) { + return false; + } + return requiredSpace <= stat.freeBytes; + }, + + findUpdateDirWithFreeSpace: function dp_findUpdateDirWithFreeSpace(requiredSpace, subdir) { + if (!Services.volumeService) { + return this.createUpdatesDir(LOCAL_DIR, subdir); + } + + let activeUpdate = Services.um.activeUpdate; + if (gUseSDCard) { + if (this.volumeHasFreeSpace(gExtStorage, requiredSpace)) { + let extUpdateDir = this.createUpdatesDir(gExtStorage, subdir); + if (extUpdateDir !== null) { + return extUpdateDir; + } + log("Warning: " + gExtStorage + " has enough free space for update " + + activeUpdate.name + ", but is not writable"); + } + } + + if (this.volumeHasFreeSpace(LOCAL_DIR, requiredSpace)) { + let localUpdateDir = this.createUpdatesDir(LOCAL_DIR, subdir); + if (localUpdateDir !== null) { + return localUpdateDir; + } + log("Warning: " + LOCAL_DIR + " has enough free space for update " + + activeUpdate.name + ", but is not writable"); + } + + return null; + }, + + getUpdateDir: function dp_getUpdateDir(persistent, subdir, multiple) { + let defaultUpdateDir = this.getDefaultUpdateDir(); + persistent.value = false; + + let activeUpdate = Services.um.activeUpdate; + if (!activeUpdate) { + log("Warning: No active update found, using default update dir: " + + defaultUpdateDir); + return defaultUpdateDir; + } + + let selectedPatch = activeUpdate.selectedPatch; + if (!selectedPatch) { + log("Warning: No selected patch, using default update dir: " + + defaultUpdateDir); + return defaultUpdateDir; + } + + let requiredSpace = selectedPatch.size * multiple; + let updateDir = this.findUpdateDirWithFreeSpace(requiredSpace, subdir); + if (updateDir) { + return updateDir; + } + + // If we've gotten this far, there isn't enough free space to download the patch + // on either external storage or /data/local. All we can do is report the + // error and let upstream code handle it more gracefully. + log("Error: No volume found with " + requiredSpace + " bytes for downloading"+ + " update " + activeUpdate.name); + activeUpdate.errorCode = Cr.NS_ERROR_FILE_TOO_BIG; + return null; + }, + + createUpdatesDir: function dp_createUpdatesDir(root, subdir) { + let dir = Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsILocalFile); + dir.initWithPath(root); + if (!dir.isWritable()) { + log("Error: " + dir.path + " isn't writable"); + return null; + } + dir.appendRelativePath(subdir); + if (dir.exists()) { + if (dir.isDirectory() && dir.isWritable()) { + return dir; + } + // subdir is either a file or isn't writable. In either case we + // can't use it. + log("Error: " + dir.path + " is a file or isn't writable"); + return null; + } + // subdir doesn't exist, and the parent is writable, so try to + // create it. This can fail if a file named updates exists. + try { + dir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt('0770', 8)); + } catch (e) { + // The create failed for some reason. We can't use it. + log("Error: " + dir.path + " unable to create directory"); + return null; + } + return dir; + }, + + getDefaultUpdateDir: function dp_getDefaultUpdateDir() { + let path = gExtStorage; + if (!path) { + path = LOCAL_DIR; + } + + if (Services.volumeService) { + let extVolume = Services.volumeService.createOrGetVolumeByPath(path); + if (!extVolume) { + path = LOCAL_DIR; + } + } + + let dir = Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsILocalFile) + dir.initWithPath(path); + + if (!dir.exists() && path != LOCAL_DIR) { + // Fallback to LOCAL_DIR if we didn't fallback earlier + dir.initWithPath(LOCAL_DIR); + + if (!dir.exists()) { + throw Cr.NS_ERROR_FILE_NOT_FOUND; + } + } + + dir.appendRelativePath("updates"); + return dir; + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DirectoryProvider]); diff --git a/b2g/components/ErrorPage.jsm b/b2g/components/ErrorPage.jsm new file mode 100644 index 000000000..2c0c64c21 --- /dev/null +++ b/b2g/components/ErrorPage.jsm @@ -0,0 +1,187 @@ +/* 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 = ['ErrorPage']; + +const Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; +const kErrorPageFrameScript = 'chrome://b2g/content/ErrorPage.js'; + +Cu.import('resource://gre/modules/Services.jsm'); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyGetter(this, "CertOverrideService", function () { + return Cc["@mozilla.org/security/certoverride;1"] + .getService(Ci.nsICertOverrideService); +}); + +/** + * A class to add exceptions to override SSL certificate problems. + * The functionality itself is borrowed from exceptionDialog.js. + */ +function SSLExceptions(aCallback, aUri, aWindow) { + this._finishCallback = aCallback; + this._uri = aUri; + this._window = aWindow; +}; + +SSLExceptions.prototype = { + _finishCallback: null, + _window: null, + _uri: null, + _temporary: null, + _sslStatus: null, + + getInterface: function SSLE_getInterface(aIID) { + return this.QueryInterface(aIID); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIBadCertListener2]), + + /** + * To collect the SSL status we intercept the certificate error here + * and store the status for later use. + */ + notifyCertProblem: function SSLE_notifyCertProblem(aSocketInfo, + aSslStatus, + aTargetHost) { + this._sslStatus = aSslStatus.QueryInterface(Ci.nsISSLStatus); + Services.tm.currentThread.dispatch({ + run: this._addOverride.bind(this) + }, Ci.nsIThread.DISPATCH_NORMAL); + return true; // suppress error UI + }, + + /** + * Attempt to download the certificate for the location specified to get + * the SSLState for the certificate and the errors. + */ + _checkCert: function SSLE_checkCert() { + this._sslStatus = null; + if (!this._uri) { + return; + } + let req = new this._window.XMLHttpRequest(); + try { + req.open("GET", this._uri.prePath, true); + req.channel.notificationCallbacks = this; + let xhrHandler = (function() { + req.removeEventListener("load", xhrHandler); + req.removeEventListener("error", xhrHandler); + if (!this._sslStatus) { + // Got response from server without an SSL error. + if (this._finishCallback) { + this._finishCallback(); + } + } + }).bind(this); + req.addEventListener("load", xhrHandler); + req.addEventListener("error", xhrHandler); + req.send(null); + } catch (e) { + // We *expect* exceptions if there are problems with the certificate + // presented by the site. Log it, just in case, but we can proceed here, + // with appropriate sanity checks + Components.utils.reportError("Attempted to connect to a site with a bad certificate in the add exception dialog. " + + "This results in a (mostly harmless) exception being thrown. " + + "Logged for information purposes only: " + e); + } + }, + + /** + * Internal method to create an override. + */ + _addOverride: function SSLE_addOverride() { + let SSLStatus = this._sslStatus; + let uri = this._uri; + let flags = 0; + + if (SSLStatus.isUntrusted) { + flags |= Ci.nsICertOverrideService.ERROR_UNTRUSTED; + } + if (SSLStatus.isDomainMismatch) { + flags |= Ci.nsICertOverrideService.ERROR_MISMATCH; + } + if (SSLStatus.isNotValidAtThisTime) { + flags |= Ci.nsICertOverrideService.ERROR_TIME; + } + + CertOverrideService.rememberValidityOverride( + uri.asciiHost, + uri.port, + SSLStatus.serverCert, + flags, + this._temporary); + + if (this._finishCallback) { + this._finishCallback(); + } + }, + + /** + * Creates a permanent exception to override all overridable errors for + * the given URL. + */ + addException: function SSLE_addException(aTemporary) { + this._temporary = aTemporary; + this._checkCert(); + } +}; + +var ErrorPage = { + _addCertException: function(aMessage) { + let frameLoaderOwner = aMessage.target.QueryInterface(Ci.nsIFrameLoaderOwner); + let win = frameLoaderOwner.ownerDocument.defaultView; + let mm = frameLoaderOwner.frameLoader.messageManager; + + let uri = Services.io.newURI(aMessage.data.url, null, null); + let sslExceptions = new SSLExceptions((function() { + mm.sendAsyncMessage('ErrorPage:ReloadPage'); + }).bind(this), uri, win); + try { + sslExceptions.addException(!aMessage.data.isPermanent); + } catch (e) { + dump("Failed to set cert exception: " + e + "\n"); + } + }, + + _listenError: function(frameLoader) { + let self = this; + let frameElement = frameLoader.ownerElement; + let injectErrorPageScript = function() { + let mm = frameLoader.messageManager; + try { + mm.loadFrameScript(kErrorPageFrameScript, true, true); + } catch (e) { + dump('Error loading ' + kErrorPageFrameScript + ' as frame script: ' + e + '\n'); + } + mm.addMessageListener('ErrorPage:AddCertException', self._addCertException.bind(self)); + frameElement.removeEventListener('mozbrowsererror', injectErrorPageScript, true); + }; + + frameElement.addEventListener('mozbrowsererror', + injectErrorPageScript, + true // use capture + ); + }, + + init: function errorPageInit() { + Services.obs.addObserver(this, 'inprocess-browser-shown', false); + Services.obs.addObserver(this, 'remote-browser-shown', false); + }, + + observe: function errorPageObserve(aSubject, aTopic, aData) { + let frameLoader = aSubject.QueryInterface(Ci.nsIFrameLoader); + // Ignore notifications that aren't from a BrowserOrApp + if (!frameLoader.ownerIsMozBrowserOrAppFrame) { + return; + } + this._listenError(frameLoader); + } +}; + +ErrorPage.init(); diff --git a/b2g/components/FilePicker.js b/b2g/components/FilePicker.js new file mode 100644 index 000000000..803eef681 --- /dev/null +++ b/b2g/components/FilePicker.js @@ -0,0 +1,223 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * No magic constructor behaviour, as is de rigeur for XPCOM. + * If you must perform some initialization, and it could possibly fail (even + * due to an out-of-memory condition), you should use an Init method, which + * can convey failure appropriately (thrown exception in JS, + * NS_FAILED(nsresult) return in C++). + * + * In JS, you can actually cheat, because a thrown exception will cause the + * CreateInstance call to fail in turn, but not all languages are so lucky. + * (Though ANSI C++ provides exceptions, they are verboten in Mozilla code + * for portability reasons -- and even when you're building completely + * platform-specific code, you can't throw across an XPCOM method boundary.) + */ + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +// FIXME: improve this list of filters. +const IMAGE_FILTERS = ['image/gif', 'image/jpeg', 'image/pjpeg', + 'image/png', 'image/svg+xml', 'image/tiff', + 'image/vnd.microsoft.icon']; +const VIDEO_FILTERS = ['video/mpeg', 'video/mp4', 'video/ogg', + 'video/quicktime', 'video/webm', 'video/x-matroska', + 'video/x-ms-wmv', 'video/x-flv']; +const AUDIO_FILTERS = ['audio/basic', 'audio/L24', 'audio/mp4', + 'audio/mpeg', 'audio/ogg', 'audio/vorbis', + 'audio/vnd.rn-realaudio', 'audio/vnd.wave', + 'audio/webm']; + +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +Cu.import("resource://gre/modules/osfile.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, 'cpmm', + '@mozilla.org/childprocessmessagemanager;1', + 'nsIMessageSender'); + +function FilePicker() { +} + +FilePicker.prototype = { + classID: Components.ID('{436ff8f9-0acc-4b11-8ec7-e293efba3141}'), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFilePicker]), + + /* members */ + + mParent: undefined, + mExtraProps: undefined, + mFilterTypes: undefined, + mFileEnumerator: undefined, + mFilePickerShownCallback: undefined, + + /* methods */ + + init: function(parent, title, mode) { + this.mParent = parent; + this.mExtraProps = {}; + this.mFilterTypes = []; + this.mMode = mode; + + if (mode != Ci.nsIFilePicker.modeOpen && + mode != Ci.nsIFilePicker.modeOpenMultiple) { + throw Cr.NS_ERROR_NOT_IMPLEMENTED; + } + }, + + /* readonly attribute nsILocalFile file - not implemented; */ + /* readonly attribute nsISimpleEnumerator files - not implemented; */ + /* readonly attribute nsIURI fileURL - not implemented; */ + + get domFileOrDirectoryEnumerator() { + return this.mFilesEnumerator; + }, + + // We don't support directory selection yet. + get domFileOrDirectory() { + return this.mFilesEnumerator ? this.mFilesEnumerator.mFiles[0] : null; + }, + + get mode() { + return this.mMode; + }, + + appendFilters: function(filterMask) { + // Ci.nsIFilePicker.filterHTML is not supported + // Ci.nsIFilePicker.filterText is not supported + + if (filterMask & Ci.nsIFilePicker.filterImages) { + this.mFilterTypes = this.mFilterTypes.concat(IMAGE_FILTERS); + // This property is needed for the gallery app pick activity. + this.mExtraProps['nocrop'] = true; + } + + // Ci.nsIFilePicker.filterXML is not supported + // Ci.nsIFilePicker.filterXUL is not supported + // Ci.nsIFilePicker.filterApps is not supported + // Ci.nsIFilePicker.filterAllowURLs is not supported + + if (filterMask & Ci.nsIFilePicker.filterVideo) { + this.mFilterTypes = this.mFilterTypes.concat(VIDEO_FILTERS); + } + + if (filterMask & Ci.nsIFilePicker.filterAudio) { + this.mFilterTypes = this.mFilterTypes.concat(AUDIO_FILTERS); + } + + if (filterMask & Ci.nsIFilePicker.filterAll) { + // This property is needed for the gallery app pick activity. + this.mExtraProps['nocrop'] = true; + } + }, + + appendFilter: function(title, extensions) { + // pick activity doesn't support extensions + }, + + open: function(aFilePickerShownCallback) { + this.mFilePickerShownCallback = aFilePickerShownCallback; + + cpmm.addMessageListener('file-picked', this); + + let detail = {}; + if (this.mFilterTypes) { + detail.type = this.mFilterTypes; + } + + for (let prop in this.mExtraProps) { + if (!(prop in detail)) { + detail[prop] = this.mExtraProps[prop]; + } + } + + cpmm.sendAsyncMessage('file-picker', detail); + }, + + fireSuccess: function(file) { + this.mFilesEnumerator = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator]), + + mFiles: [file], + mIndex: 0, + + hasMoreElements: function() { + return (this.mIndex < this.mFiles.length); + }, + + getNext: function() { + if (this.mIndex >= this.mFiles.length) { + throw Components.results.NS_ERROR_FAILURE; + } + return this.mFiles[this.mIndex++]; + } + }; + + if (this.mFilePickerShownCallback) { + this.mFilePickerShownCallback.done(Ci.nsIFilePicker.returnOK); + this.mFilePickerShownCallback = null; + } + }, + + fireError: function() { + if (this.mFilePickerShownCallback) { + this.mFilePickerShownCallback.done(Ci.nsIFilePicker.returnCancel); + this.mFilePickerShownCallback = null; + } + }, + + receiveMessage: function(message) { + if (message.name !== 'file-picked') { + return; + } + + cpmm.removeMessageListener('file-picked', this); + + let data = message.data; + if (!data.success || !data.result.blob) { + this.fireError(); + return; + } + + // The name to be shown can be part of the message, or can be taken from + // the File (if the blob is a File). + let name = data.result.name; + if (!name && + (data.result.blob instanceof this.mParent.File) && + data.result.blob.name) { + name = data.result.blob.name; + } + + // Let's try to remove the full path and take just the filename. + if (name) { + let names = OS.Path.split(name); + name = names.components[names.components.length - 1]; + } + + // the fallback is a filename composed by 'blob' + extension. + if (!name) { + name = 'blob'; + if (data.result.blob.type) { + let mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + let mimeInfo = mimeSvc.getFromTypeAndExtension(data.result.blob.type, ''); + if (mimeInfo) { + name += '.' + mimeInfo.primaryExtension; + } + } + } + + let file = new this.mParent.File([data.result.blob], + name, + { type: data.result.blob.type }); + + if (file) { + this.fireSuccess(file); + } else { + this.fireError(); + } + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FilePicker]); diff --git a/b2g/components/Frames.jsm b/b2g/components/Frames.jsm new file mode 100644 index 000000000..0eb00cb4c --- /dev/null +++ b/b2g/components/Frames.jsm @@ -0,0 +1,146 @@ +/* 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 = ['Frames']; + +const Cu = Components.utils; +const Ci = Components.interfaces; + +Cu.import('resource://gre/modules/Services.jsm'); +Cu.import('resource://gre/modules/SystemAppProxy.jsm'); + +const listeners = []; + +const Observer = { + // Save a map of (MessageManager => Frame) to be able to dispatch + // the FrameDestroyed event with a frame reference. + _frames: new Map(), + + // Also save current number of iframes opened by app + _apps: new Map(), + + start: function () { + Services.obs.addObserver(this, 'remote-browser-shown', false); + Services.obs.addObserver(this, 'inprocess-browser-shown', false); + Services.obs.addObserver(this, 'message-manager-close', false); + + SystemAppProxy.getFrames().forEach(frame => { + let mm = frame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager; + this._frames.set(mm, frame); + let mozapp = frame.getAttribute('mozapp'); + if (mozapp) { + this._apps.set(mozapp, (this._apps.get(mozapp) || 0) + 1); + } + }); + }, + + stop: function () { + Services.obs.removeObserver(this, 'remote-browser-shown'); + Services.obs.removeObserver(this, 'inprocess-browser-shown'); + Services.obs.removeObserver(this, 'message-manager-close'); + this._frames.clear(); + this._apps.clear(); + }, + + observe: function (subject, topic, data) { + switch(topic) { + + // Listen for frame creation in OOP (device) as well as in parent process (b2g desktop) + case 'remote-browser-shown': + case 'inprocess-browser-shown': + let frameLoader = subject; + + // get a ref to the app <iframe> + frameLoader.QueryInterface(Ci.nsIFrameLoader); + let frame = frameLoader.ownerElement; + let mm = frame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager; + this.onMessageManagerCreated(mm, frame); + break; + + // Every time an iframe is destroyed, its message manager also is + case 'message-manager-close': + this.onMessageManagerDestroyed(subject); + break; + } + }, + + onMessageManagerCreated: function (mm, frame) { + this._frames.set(mm, frame); + + let isFirstAppFrame = null; + let mozapp = frame.getAttribute('mozapp'); + if (mozapp) { + let count = (this._apps.get(mozapp) || 0) + 1; + this._apps.set(mozapp, count); + isFirstAppFrame = (count === 1); + } + + listeners.forEach(function (listener) { + try { + listener.onFrameCreated(frame, isFirstAppFrame); + } catch(e) { + dump('Exception while calling Frames.jsm listener:' + e + '\n' + + e.stack + '\n'); + } + }); + }, + + onMessageManagerDestroyed: function (mm) { + let frame = this._frames.get(mm); + if (!frame) { + // We received an event for an unknown message manager + return; + } + + this._frames.delete(mm); + + let isLastAppFrame = null; + let mozapp = frame.getAttribute('mozapp'); + if (mozapp) { + let count = (this._apps.get(mozapp) || 0) - 1; + this._apps.set(mozapp, count); + isLastAppFrame = (count === 0); + } + + listeners.forEach(function (listener) { + try { + listener.onFrameDestroyed(frame, isLastAppFrame); + } catch(e) { + dump('Exception while calling Frames.jsm listener:' + e + '\n' + + e.stack + '\n'); + } + }); + } + +}; + +var Frames = this.Frames = { + + list: () => SystemAppProxy.getFrames(), + + addObserver: function (listener) { + if (listeners.indexOf(listener) !== -1) { + return; + } + + listeners.push(listener); + if (listeners.length == 1) { + Observer.start(); + } + }, + + removeObserver: function (listener) { + let idx = listeners.indexOf(listener); + if (idx !== -1) { + listeners.splice(idx, 1); + } + if (listeners.length === 0) { + Observer.stop(); + } + } + +}; + diff --git a/b2g/components/FxAccountsMgmtService.jsm b/b2g/components/FxAccountsMgmtService.jsm new file mode 100644 index 000000000..e51f46ed7 --- /dev/null +++ b/b2g/components/FxAccountsMgmtService.jsm @@ -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/. */ + +/** + * Some specific (certified) apps need to get access to certain Firefox Accounts + * functionality that allows them to manage accounts (this is mostly sign up, + * sign in, logout and delete) and get information about the currently existing + * ones. + * + * This service listens for requests coming from these apps, triggers the + * appropriate Fx Accounts flows and send reponses back to the UI. + * + * The communication mechanism is based in mozFxAccountsContentEvent (for + * messages coming from the UI) and mozFxAccountsChromeEvent (for messages + * sent from the chrome side) custom events. + */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["FxAccountsMgmtService"]; + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/FxAccountsCommon.js"); + +XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsManager", + "resource://gre/modules/FxAccountsManager.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy", + "resource://gre/modules/SystemAppProxy.jsm"); + +this.FxAccountsMgmtService = { + _onFulfill: function(aMsgId, aData) { + SystemAppProxy._sendCustomEvent("mozFxAccountsChromeEvent", { + id: aMsgId, + data: aData ? aData : null + }); + }, + + _onReject: function(aMsgId, aReason) { + SystemAppProxy._sendCustomEvent("mozFxAccountsChromeEvent", { + id: aMsgId, + error: aReason ? aReason : null + }); + }, + + init: function() { + Services.obs.addObserver(this, ONLOGIN_NOTIFICATION, false); + Services.obs.addObserver(this, ONVERIFIED_NOTIFICATION, false); + Services.obs.addObserver(this, ONLOGOUT_NOTIFICATION, false); + SystemAppProxy.addEventListener("mozFxAccountsContentEvent", + FxAccountsMgmtService); + }, + + observe: function(aSubject, aTopic, aData) { + log.debug("Observed " + aTopic); + switch (aTopic) { + case ONLOGIN_NOTIFICATION: + case ONVERIFIED_NOTIFICATION: + case ONLOGOUT_NOTIFICATION: + // FxAccounts notifications have the form of fxaccounts:* + SystemAppProxy._sendCustomEvent("mozFxAccountsUnsolChromeEvent", { + eventName: aTopic.substring(aTopic.indexOf(":") + 1) + }); + break; + } + }, + + handleEvent: function(aEvent) { + let msg = aEvent.detail; + log.debug("MgmtService got content event: " + JSON.stringify(msg)); + let self = FxAccountsMgmtService; + + if (!msg.id) { + return; + } + + if (msg.error) { + self._onReject(msg.id, msg.error); + return; + } + + let data = msg.data; + if (!data) { + return; + } + // Backwards compatibility: handle accountId coming from Gaia + if (data.accountId && typeof(data.email === "undefined")) { + data.email = data.accountId; + delete data.accountId; + } + + // Bug 1202450 dirty hack because Gaia is sending getAccounts. + if (data.method == "getAccounts") { + data.method = "getAccount"; + } + + switch(data.method) { + case "getAssertion": + let principal = Services.scriptSecurityManager.getSystemPrincipal(); + let audience = data.audience || principal.originNoSuffix; + FxAccountsManager.getAssertion(audience, principal, { + silent: msg.silent || false + }).then(result => { + self._onFulfill(msg.id, result); + }, reason => { + self._onReject(msg.id, reason); + }); + break; + case "getAccount": + case "getKeys": + FxAccountsManager[data.method]().then( + result => { + // For the getAccounts case, we only expose the email and + // verification status so far. + self._onFulfill(msg.id, result); + }, + reason => { + self._onReject(msg.id, reason); + } + ).then(null, Components.utils.reportError); + break; + case "logout": + FxAccountsManager.signOut().then( + () => { + self._onFulfill(msg.id); + }, + reason => { + self._onReject(msg.id, reason); + } + ).then(null, Components.utils.reportError); + break; + case "queryAccount": + FxAccountsManager.queryAccount(data.email).then( + result => { + self._onFulfill(msg.id, result); + }, + reason => { + self._onReject(msg.id, reason); + } + ).then(null, Components.utils.reportError); + break; + case "resendVerificationEmail": + FxAccountsManager.resendVerificationEmail().then( + () => { + self._onFulfill(msg.id); + }, + reason => { + self._onReject(msg.id, reason); + } + ).then(null, Components.utils.reportError); + break; + case "signIn": + case "signUp": + case "refreshAuthentication": + FxAccountsManager[data.method](data.email, data.password, + data.fetchKeys).then( + user => { + self._onFulfill(msg.id, user); + }, + reason => { + self._onReject(msg.id, reason); + } + ).then(null, Components.utils.reportError); + break; + } + } +}; + +FxAccountsMgmtService.init(); diff --git a/b2g/components/FxAccountsUIGlue.js b/b2g/components/FxAccountsUIGlue.js new file mode 100644 index 000000000..d62a7d14f --- /dev/null +++ b/b2g/components/FxAccountsUIGlue.js @@ -0,0 +1,39 @@ +/* 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 } = Components; + +Cu.import("resource://gre/modules/FxAccountsCommon.js"); +Cu.import("resource://gre/modules/ContentRequestHelper.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +function FxAccountsUIGlue() { +} + +FxAccountsUIGlue.prototype = { + + __proto__: ContentRequestHelper.prototype, + + signInFlow: function() { + return this.contentRequest("mozFxAccountsRPContentEvent", + "mozFxAccountsUnsolChromeEvent", + "openFlow"); + }, + + refreshAuthentication: function(aEmail) { + return this.contentRequest("mozFxAccountsRPContentEvent", + "mozFxAccountsUnsolChromeEvent", + "refreshAuthentication", { + email: aEmail + }); + }, + + classID: Components.ID("{51875c14-91d7-4b8c-b65d-3549e101228c}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIFxAccountsUIGlue]) +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([FxAccountsUIGlue]); diff --git a/b2g/components/GaiaChrome.cpp b/b2g/components/GaiaChrome.cpp new file mode 100644 index 000000000..2b53750c2 --- /dev/null +++ b/b2g/components/GaiaChrome.cpp @@ -0,0 +1,188 @@ +/* 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 "GaiaChrome.h" + +#include "nsAppDirectoryServiceDefs.h" +#include "nsChromeRegistry.h" +#include "nsDirectoryServiceDefs.h" +#include "nsLocalFile.h" +#include "nsXULAppAPI.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/ModuleUtils.h" +#include "mozilla/Services.h" +#include "mozilla/FileLocation.h" + +#define NS_GAIACHROME_CID \ + { 0x83f8f999, 0x6b87, 0x4dd8, { 0xa0, 0x93, 0x72, 0x0b, 0xfb, 0x67, 0x4d, 0x38 } } + +using namespace mozilla; + +StaticRefPtr<GaiaChrome> gGaiaChrome; + +NS_IMPL_ISUPPORTS(GaiaChrome, nsIGaiaChrome) + +GaiaChrome::GaiaChrome() + : mPackageName(NS_LITERAL_CSTRING("gaia")) + , mAppsDir(NS_LITERAL_STRING("apps")) + , mDataRoot(NS_LITERAL_STRING("/data/local")) + , mSystemRoot(NS_LITERAL_STRING("/system/b2g")) +{ + MOZ_ASSERT(NS_IsMainThread()); + + GetProfileDir(); + Register(); +} + +//virtual +GaiaChrome::~GaiaChrome() +{ +} + +nsresult +GaiaChrome::GetProfileDir() +{ + nsCOMPtr<nsIFile> profDir; + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(profDir)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = profDir->Clone(getter_AddRefs(mProfDir)); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +GaiaChrome::ComputeAppsPath(nsIFile* aPath) +{ +#if defined(MOZ_MULET) + aPath->InitWithFile(mProfDir); +#elif defined(MOZ_WIDGET_GONK) + nsCOMPtr<nsIFile> locationDetection = new nsLocalFile(); + locationDetection->InitWithPath(mSystemRoot); + locationDetection->Append(mAppsDir); + bool appsInSystem = EnsureIsDirectory(locationDetection); + locationDetection->InitWithPath(mDataRoot); + locationDetection->Append(mAppsDir); + bool appsInData = EnsureIsDirectory(locationDetection); + + if (!appsInData && !appsInSystem) { + printf_stderr("!!! NO root directory with apps found\n"); + MOZ_ASSERT(false); + return NS_ERROR_UNEXPECTED; + } + + aPath->InitWithPath(appsInData ? mDataRoot : mSystemRoot); +#else + return NS_ERROR_UNEXPECTED; +#endif + + aPath->Append(mAppsDir); + aPath->Append(NS_LITERAL_STRING(".")); + + nsresult rv = EnsureValidPath(aPath); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +bool +GaiaChrome::EnsureIsDirectory(nsIFile* aPath) +{ + bool isDir = false; + aPath->IsDirectory(&isDir); + return isDir; +} + +nsresult +GaiaChrome::EnsureValidPath(nsIFile* appsDir) +{ + // Ensure there is a valid "apps/system" directory + nsCOMPtr<nsIFile> systemAppDir = new nsLocalFile(); + systemAppDir->InitWithFile(appsDir); + systemAppDir->Append(NS_LITERAL_STRING("system")); + + bool hasSystemAppDir = EnsureIsDirectory(systemAppDir); + if (!hasSystemAppDir) { + nsCString path; appsDir->GetNativePath(path); + // We don't want to continue if the apps path does not exists ... + printf_stderr("!!! Gaia chrome package is not a directory: %s\n", path.get()); + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +NS_IMETHODIMP +GaiaChrome::Register() +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(nsChromeRegistry::gChromeRegistry != nullptr); + + nsCOMPtr<nsIFile> aPath = new nsLocalFile(); + nsresult rv = ComputeAppsPath(aPath); + NS_ENSURE_SUCCESS(rv, rv); + + FileLocation appsLocation(aPath); + nsCString uri; + appsLocation.GetURIString(uri); + + char* argv[2]; + argv[0] = (char*)mPackageName.get(); + argv[1] = (char*)uri.get(); + + nsChromeRegistry::ManifestProcessingContext cx(NS_APP_LOCATION, appsLocation); + nsChromeRegistry::gChromeRegistry->ManifestContent(cx, 0, argv, 0); + + return NS_OK; +} + +already_AddRefed<GaiaChrome> +GaiaChrome::FactoryCreate() +{ + if (!XRE_IsParentProcess()) { + return nullptr; + } + + MOZ_ASSERT(NS_IsMainThread()); + + if (!gGaiaChrome) { + gGaiaChrome = new GaiaChrome(); + ClearOnShutdown(&gGaiaChrome); + } + + RefPtr<GaiaChrome> service = gGaiaChrome.get(); + return service.forget(); +} + +NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(GaiaChrome, + GaiaChrome::FactoryCreate) + +NS_DEFINE_NAMED_CID(NS_GAIACHROME_CID); + +static const mozilla::Module::CIDEntry kGaiaChromeCIDs[] = { + { &kNS_GAIACHROME_CID, false, nullptr, GaiaChromeConstructor }, + { nullptr } +}; + +static const mozilla::Module::ContractIDEntry kGaiaChromeContracts[] = { + { "@mozilla.org/b2g/gaia-chrome;1", &kNS_GAIACHROME_CID }, + { nullptr } +}; + +static const mozilla::Module::CategoryEntry kGaiaChromeCategories[] = { + { "profile-after-change", "Gaia Chrome Registration", GAIACHROME_CONTRACTID }, + { nullptr } +}; + +static const mozilla::Module kGaiaChromeModule = { + mozilla::Module::kVersion, + kGaiaChromeCIDs, + kGaiaChromeContracts, + kGaiaChromeCategories +}; + +NSMODULE_DEFN(GaiaChromeModule) = &kGaiaChromeModule; diff --git a/b2g/components/GaiaChrome.h b/b2g/components/GaiaChrome.h new file mode 100644 index 000000000..290613b81 --- /dev/null +++ b/b2g/components/GaiaChrome.h @@ -0,0 +1,44 @@ +/* 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/. */ + +#ifndef __GAIACHROME_H__ +#define __GAIACHROME_H__ + +#include "nsIGaiaChrome.h" + +#include "nsIFile.h" + +#include "nsCOMPtr.h" +#include "nsString.h" + +using namespace mozilla; + +class GaiaChrome final : public nsIGaiaChrome +{ +public: + NS_DECL_ISUPPORTS + NS_DECL_NSIGAIACHROME + + static already_AddRefed<GaiaChrome> + FactoryCreate(); + +private: + nsCString mPackageName; + + nsAutoString mAppsDir; + nsAutoString mDataRoot; + nsAutoString mSystemRoot; + + nsCOMPtr<nsIFile> mProfDir; + + GaiaChrome(); + ~GaiaChrome(); + + nsresult ComputeAppsPath(nsIFile*); + bool EnsureIsDirectory(nsIFile*); + nsresult EnsureValidPath(nsIFile*); + nsresult GetProfileDir(); +}; + +#endif // __GAIACHROME_H__ diff --git a/b2g/components/GlobalSimulatorScreen.jsm b/b2g/components/GlobalSimulatorScreen.jsm new file mode 100644 index 000000000..2895aef96 --- /dev/null +++ b/b2g/components/GlobalSimulatorScreen.jsm @@ -0,0 +1,90 @@ +/* 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/. */ + +this.EXPORTED_SYMBOLS = [ 'GlobalSimulatorScreen' ]; + +const Cu = Components.utils; + +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +Cu.import('resource://gre/modules/Services.jsm'); + +this.GlobalSimulatorScreen = { + mozOrientationLocked: false, + + // Actual orientation of apps + mozOrientation: 'portrait', + + // The restricted list of actual orientation that can be used + // if mozOrientationLocked is true + lockedOrientation: [], + + // The faked screen orientation + // if screenOrientation doesn't match mozOrientation due + // to lockedOrientation restriction, the app will be displayed + // on the side on desktop + screenOrientation: 'portrait', + + // Updated by screen.js + width: 0, height: 0, + + lock: function(orientation) { + this.mozOrientationLocked = true; + + // Normalize to portrait or landscape, + // i.e. the possible values of screenOrientation + function normalize(str) { + if (str.match(/^portrait/)) { + return 'portrait'; + } else if (str.match(/^landscape/)) { + return 'landscape'; + } else { + return 'portrait'; + } + } + this.lockedOrientation = orientation.map(normalize); + + this.updateOrientation(); + }, + + unlock: function() { + this.mozOrientationLocked = false; + this.updateOrientation(); + }, + + updateOrientation: function () { + let orientation = this.screenOrientation; + + // If the orientation is locked, we have to ensure ending up with a value + // of lockedOrientation. If none of lockedOrientation values matches + // the screen orientation we just choose the first locked orientation. + // This will be the precise scenario where the app is displayed on the + // side on desktop! + if (this.mozOrientationLocked && + this.lockedOrientation.indexOf(this.screenOrientation) == -1) { + orientation = this.lockedOrientation[0]; + } + + // If the actual orientation changed, + // we have to fire mozorientation DOM events + if (this.mozOrientation != orientation) { + this.mozOrientation = orientation; + + // Notify each app screen object to fire the event + Services.obs.notifyObservers(null, 'simulator-orientation-change', null); + } + + // Finally, in any case, we update the window size and orientation + // (Use wrappedJSObject trick to be able to pass a raw JS object) + Services.obs.notifyObservers({wrappedJSObject:this}, 'simulator-adjust-window-size', null); + }, + + flipScreen: function() { + if (this.screenOrientation == 'portrait') { + this.screenOrientation = 'landscape'; + } else if (this.screenOrientation == 'landscape') { + this.screenOrientation = 'portrait'; + } + this.updateOrientation(); + } +} diff --git a/b2g/components/HelperAppDialog.js b/b2g/components/HelperAppDialog.js new file mode 100644 index 000000000..3709833e1 --- /dev/null +++ b/b2g/components/HelperAppDialog.js @@ -0,0 +1,115 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Downloads", + "resource://gre/modules/Downloads.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Task", + "resource://gre/modules/Task.jsm"); + +// ----------------------------------------------------------------------- +// HelperApp Launcher Dialog +// +// For now on b2g we never prompt and just download to the default +// location. +// +// ----------------------------------------------------------------------- + +function HelperAppLauncherDialog() { } + +HelperAppLauncherDialog.prototype = { + classID: Components.ID("{710322af-e6ae-4b0c-b2c9-1474a87b077e}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]), + + show: function(aLauncher, aContext, aReason) { + aLauncher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.saveToDisk; + aLauncher.saveToDisk(null, false); + }, + + promptForSaveToFileAsync: function(aLauncher, + aContext, + aDefaultFile, + aSuggestedFileExt, + aForcePrompt) { + // Retrieve the user's default download directory. + Task.spawn(function*() { + let file = null; + try { + let defaultFolder = yield Downloads.getPreferredDownloadsDirectory(); + let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + dir.initWithPath(defaultFolder); + file = this.validateLeafName(dir, aDefaultFile, aSuggestedFileExt); + } catch(e) { } + aLauncher.saveDestinationAvailable(file); + }.bind(this)).then(null, Cu.reportError); + }, + + validateLeafName: function(aLocalFile, aLeafName, aFileExt) { + if (!(aLocalFile && this.isUsableDirectory(aLocalFile))) + return null; + + // Remove any leading periods, since we don't want to save hidden files + // automatically. + aLeafName = aLeafName.replace(/^\.+/, ""); + + if (aLeafName == "") + aLeafName = "unnamed" + (aFileExt ? "." + aFileExt : ""); + aLocalFile.append(aLeafName); + + this.makeFileUnique(aLocalFile); + return aLocalFile; + }, + + makeFileUnique: function(aLocalFile) { + try { + // Note - this code is identical to that in + // toolkit/content/contentAreaUtils.js. + // If you are updating this code, update that code too! We can't share code + // here since this is called in a js component. + let collisionCount = 0; + while (aLocalFile.exists()) { + collisionCount++; + if (collisionCount == 1) { + // Append "(2)" before the last dot in (or at the end of) the filename + // special case .ext.gz etc files so we don't wind up with .tar(2).gz + if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i)) + aLocalFile.leafName = aLocalFile.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&"); + else + aLocalFile.leafName = aLocalFile.leafName.replace(/(\.[^\.]*)?$/, "(2)$&"); + } + else { + // replace the last (n) in the filename with (n+1) + aLocalFile.leafName = aLocalFile.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount+1) + ")"); + } + } + aLocalFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + } + catch (e) { + dump("*** exception in makeFileUnique: " + e + "\n"); + + if (e.result == Cr.NS_ERROR_FILE_ACCESS_DENIED) + throw e; + + if (aLocalFile.leafName == "" || aLocalFile.isDirectory()) { + aLocalFile.append("unnamed"); + if (aLocalFile.exists()) + aLocalFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + } + } + }, + + isUsableDirectory: function(aDirectory) { + return aDirectory.exists() && + aDirectory.isDirectory() && + aDirectory.isWritable(); + }, +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([HelperAppLauncherDialog]); diff --git a/b2g/components/LogCapture.jsm b/b2g/components/LogCapture.jsm new file mode 100644 index 000000000..803028d57 --- /dev/null +++ b/b2g/components/LogCapture.jsm @@ -0,0 +1,221 @@ +/* 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/. */ +/* jshint moz: true */ +/* global Uint8Array, Components, dump */ + +"use strict"; + +const Cu = Components.utils; +const Ci = Components.interfaces; +const Cc = Components.classes; + +Cu.importGlobalProperties(['FileReader']); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/Promise.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Screenshot", "resource://gre/modules/Screenshot.jsm"); + +this.EXPORTED_SYMBOLS = ["LogCapture"]; + +const SYSTEM_PROPERTY_KEY_MAX = 32; +const SYSTEM_PROPERTY_VALUE_MAX = 92; + +function debug(msg) { + dump("LogCapture.jsm: " + msg + "\n"); +} + +var LogCapture = { + ensureLoaded: function() { + if (!this.ctypes) { + this.load(); + } + }, + + load: function() { + // load in everything on first use + Cu.import("resource://gre/modules/ctypes.jsm", this); + + this.libc = this.ctypes.open(this.ctypes.libraryName("c")); + + this.read = this.libc.declare("read", + this.ctypes.default_abi, + this.ctypes.int, // bytes read (out) + this.ctypes.int, // file descriptor (in) + this.ctypes.voidptr_t, // buffer to read into (in) + this.ctypes.size_t // size_t size of buffer (in) + ); + + this.open = this.libc.declare("open", + this.ctypes.default_abi, + this.ctypes.int, // file descriptor (returned) + this.ctypes.char.ptr, // path + this.ctypes.int // flags + ); + + this.close = this.libc.declare("close", + this.ctypes.default_abi, + this.ctypes.int, // error code (returned) + this.ctypes.int // file descriptor + ); + + this.getpid = this.libc.declare("getpid", + this.ctypes.default_abi, + this.ctypes.int // PID + ); + + this.property_find_nth = + this.libc.declare("__system_property_find_nth", + this.ctypes.default_abi, + this.ctypes.voidptr_t, // return value: nullable prop_info* + this.ctypes.unsigned_int); // n: the index of the property to return + + this.property_read = + this.libc.declare("__system_property_read", + this.ctypes.default_abi, + this.ctypes.void_t, // return: none + this.ctypes.voidptr_t, // non-null prop_info* + this.ctypes.char.ptr, // key + this.ctypes.char.ptr); // value + + this.key_buf = this.ctypes.char.array(SYSTEM_PROPERTY_KEY_MAX)(); + this.value_buf = this.ctypes.char.array(SYSTEM_PROPERTY_VALUE_MAX)(); + }, + + cleanup: function() { + this.libc.close(); + + this.read = null; + this.open = null; + this.close = null; + this.property_find_nth = null; + this.property_read = null; + this.key_buf = null; + this.value_buf = null; + + this.libc = null; + this.ctypes = null; + }, + + /** + * readLogFile + * Read in /dev/log/{{log}} in nonblocking mode, which will return -1 if + * reading would block the thread. + * + * @param log {String} The log from which to read. Must be present in /dev/log + * @return {Uint8Array} Raw log data + */ + readLogFile: function(logLocation) { + this.ensureLoaded(); + + const O_READONLY = 0; + const O_NONBLOCK = 1 << 11; + + const BUF_SIZE = 2048; + + let BufType = this.ctypes.ArrayType(this.ctypes.char); + let buf = new BufType(BUF_SIZE); + let logArray = []; + + let logFd = this.open(logLocation, O_READONLY | O_NONBLOCK); + if (logFd === -1) { + return null; + } + + let readStart = Date.now(); + let readCount = 0; + while (true) { + let count = this.read(logFd, buf, BUF_SIZE); + readCount += 1; + + if (count <= 0) { + // log has return due to being nonblocking or running out of things + break; + } + for(let i = 0; i < count; i++) { + logArray.push(buf[i]); + } + } + + let logTypedArray = new Uint8Array(logArray); + + this.close(logFd); + + return logTypedArray; + }, + + /** + * Get all system properties as a dict with keys mapping to values + */ + readProperties: function() { + this.ensureLoaded(); + let n = 0; + let propertyDict = {}; + + while(true) { + let prop_info = this.property_find_nth(n); + if(prop_info.isNull()) { + break; + } + + // read the prop_info into the key and value buffers + this.property_read(prop_info, this.key_buf, this.value_buf); + let key = this.key_buf.readString();; + let value = this.value_buf.readString() + + propertyDict[key] = value; + n++; + } + + return propertyDict; + }, + + /** + * Dumping about:memory to a file in /data/local/tmp/, returning a Promise. + * Will be resolved with the dumped file name. + */ + readAboutMemory: function() { + this.ensureLoaded(); + let deferred = Promise.defer(); + + // Perform the dump + let dumper = Cc["@mozilla.org/memory-info-dumper;1"] + .getService(Ci.nsIMemoryInfoDumper); + + let file = "/data/local/tmp/logshake-about_memory-" + this.getpid() + ".json.gz"; + dumper.dumpMemoryReportsToNamedFile(file, function() { + deferred.resolve(file); + }, null, false); + + return deferred.promise; + }, + + /** + * Dumping screenshot, returning a Promise. Will be resolved with the content + * as an ArrayBuffer. + */ + getScreenshot: function() { + let deferred = Promise.defer(); + try { + this.ensureLoaded(); + + let fr = new FileReader(); + fr.onload = function(evt) { + deferred.resolve(new Uint8Array(evt.target.result)); + }; + + fr.onerror = function(evt) { + deferred.reject(evt); + }; + + fr.readAsArrayBuffer(Screenshot.get()); + } catch(e) { + // We pass any errors through to the deferred Promise + deferred.reject(e); + } + + return deferred.promise; + } +}; + +this.LogCapture = LogCapture; diff --git a/b2g/components/LogParser.jsm b/b2g/components/LogParser.jsm new file mode 100644 index 000000000..c40db9767 --- /dev/null +++ b/b2g/components/LogParser.jsm @@ -0,0 +1,257 @@ +/* jshint esnext: true */ +/* global DataView */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["LogParser"]; + +/** + * Parse an array read from a /dev/log/ file. Format taken from + * kernel/drivers/staging/android/logger.h and system/core/logcat/logcat.cpp + * + * @param array {Uint8Array} Array read from /dev/log/ file + * @return {Array} List of log messages + */ +function parseLogArray(array) { + let data = new DataView(array.buffer); + let byteString = String.fromCharCode.apply(null, array); + + // Length of bytes that precede the payload of a log message + // From the 5 Uint32 and 1 Uint8 reads + const HEADER_LENGTH = 21; + + let logMessages = []; + let pos = 0; + + while (pos + HEADER_LENGTH < byteString.length) { + // Parse a single log entry + + // Track current offset from global position + let offset = 0; + + // Length of the entry, discarded + let length = data.getUint32(pos + offset, true); + offset += 4; + // Id of the process which generated the message + let processId = data.getUint32(pos + offset, true); + offset += 4; + // Id of the thread which generated the message + let threadId = data.getUint32(pos + offset, true); + offset += 4; + // Seconds since epoch when this message was logged + let seconds = data.getUint32(pos + offset, true); + offset += 4; + // Nanoseconds since the last second + let nanoseconds = data.getUint32(pos + offset, true); + offset += 4; + + // Priority in terms of the ANDROID_LOG_* constants (see below) + // This is where the length field begins counting + let priority = data.getUint8(pos + offset); + + // Reset pos and offset to count from here + pos += offset; + offset = 0; + offset += 1; + + // Read the tag and message, represented as null-terminated c-style strings + let tag = ""; + while (byteString[pos + offset] != "\0") { + tag += byteString[pos + offset]; + offset ++; + } + offset ++; + + let message = ""; + // The kernel log driver may have cut off the null byte (logprint.c) + while (byteString[pos + offset] != "\0" && offset < length) { + message += byteString[pos + offset]; + offset ++; + } + + // Un-skip the missing null terminator + if (offset === length) { + offset --; + } + + offset ++; + + pos += offset; + + // Log messages are occasionally delimited by newlines, but are also + // sometimes followed by newlines as well + if (message.charAt(message.length - 1) === "\n") { + message = message.substring(0, message.length - 1); + } + + // Add an aditional time property to mimic the milliseconds since UTC + // expected by Date + let time = seconds * 1000.0 + nanoseconds/1000000.0; + + // Log messages with interleaved newlines are considered to be separate log + // messages by logcat + for (let lineMessage of message.split("\n")) { + logMessages.push({ + processId: processId, + threadId: threadId, + seconds: seconds, + nanoseconds: nanoseconds, + time: time, + priority: priority, + tag: tag, + message: lineMessage + "\n" + }); + } + } + + return logMessages; +} + +/** + * Get a thread-time style formatted string from time + * @param time {Number} Milliseconds since epoch + * @return {String} Formatted time string + */ +function getTimeString(time) { + let date = new Date(time); + function pad(number) { + if ( number < 10 ) { + return "0" + number; + } + return number; + } + return pad( date.getMonth() + 1 ) + + "-" + pad( date.getDate() ) + + " " + pad( date.getHours() ) + + ":" + pad( date.getMinutes() ) + + ":" + pad( date.getSeconds() ) + + "." + (date.getMilliseconds() / 1000).toFixed(3).slice(2, 5); +} + +/** + * Pad a string using spaces on the left + * @param str {String} String to pad + * @param width {Number} Desired string length + */ +function padLeft(str, width) { + while (str.length < width) { + str = " " + str; + } + return str; +} + +/** + * Pad a string using spaces on the right + * @param str {String} String to pad + * @param width {Number} Desired string length + */ +function padRight(str, width) { + while (str.length < width) { + str = str + " "; + } + return str; +} + +/** Constant values taken from system/core/liblog */ +const ANDROID_LOG_UNKNOWN = 0; +const ANDROID_LOG_DEFAULT = 1; +const ANDROID_LOG_VERBOSE = 2; +const ANDROID_LOG_DEBUG = 3; +const ANDROID_LOG_INFO = 4; +const ANDROID_LOG_WARN = 5; +const ANDROID_LOG_ERROR = 6; +const ANDROID_LOG_FATAL = 7; +const ANDROID_LOG_SILENT = 8; + +/** + * Map a priority number to its abbreviated string equivalent + * @param priorityNumber {Number} Log-provided priority number + * @return {String} Priority number's abbreviation + */ +function getPriorityString(priorityNumber) { + switch (priorityNumber) { + case ANDROID_LOG_VERBOSE: + return "V"; + case ANDROID_LOG_DEBUG: + return "D"; + case ANDROID_LOG_INFO: + return "I"; + case ANDROID_LOG_WARN: + return "W"; + case ANDROID_LOG_ERROR: + return "E"; + case ANDROID_LOG_FATAL: + return "F"; + case ANDROID_LOG_SILENT: + return "S"; + default: + return "?"; + } +} + + +/** + * Mimic the logcat "threadtime" format, generating a formatted string from a + * log message object. + * @param logMessage {Object} A log message from the list returned by parseLogArray + * @return {String} threadtime formatted summary of the message + */ +function formatLogMessage(logMessage) { + // MM-DD HH:MM:SS.ms pid tid priority tag: message + // from system/core/liblog/logprint.c: + return getTimeString(logMessage.time) + + " " + padLeft(""+logMessage.processId, 5) + + " " + padLeft(""+logMessage.threadId, 5) + + " " + getPriorityString(logMessage.priority) + + " " + padRight(logMessage.tag, 8) + + ": " + logMessage.message; +} + +/** + * Convert a string to a utf-8 Uint8Array + * @param {String} str + * @return {Uint8Array} + */ +function textEncode(str) { + return new TextEncoder("utf-8").encode(str); +} + +/** + * Pretty-print an array of bytes read from a log file by parsing then + * threadtime formatting its entries. + * @param array {Uint8Array} Array of a log file's bytes + * @return {Uint8Array} Pretty-printed log + */ +function prettyPrintLogArray(array) { + let logMessages = parseLogArray(array); + return textEncode(logMessages.map(formatLogMessage).join("")); +} + +/** + * Pretty-print an array read from the list of propreties. + * @param {Object} Object representing the properties + * @return {Uint8Array} Human-readable string of property name: property value + */ +function prettyPrintPropertiesArray(properties) { + let propertiesString = ""; + for(let propName in properties) { + propertiesString += "[" + propName + "]: [" + properties[propName] + "]\n"; + } + return textEncode(propertiesString); +} + +/** + * Pretty-print a normal array. Does nothing. + * @param array {Uint8Array} Input array + * @return {Uint8Array} The same array + */ +function prettyPrintArray(array) { + return array; +} + +this.LogParser = { + parseLogArray: parseLogArray, + prettyPrintArray: prettyPrintArray, + prettyPrintLogArray: prettyPrintLogArray, + prettyPrintPropertiesArray: prettyPrintPropertiesArray +}; diff --git a/b2g/components/LogShake.jsm b/b2g/components/LogShake.jsm new file mode 100644 index 000000000..6426c21de --- /dev/null +++ b/b2g/components/LogShake.jsm @@ -0,0 +1,588 @@ +/* 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/. */ + +/** + * LogShake is a module which listens for log requests sent by Gaia. In + * response to a sufficiently large acceleration (a shake), it will save log + * files to an arbitrary directory which it will then return on a + * 'capture-logs-success' event with detail.logFilenames representing each log + * file's name and detail.logPaths representing the patch to each log file or + * the path to the archive. + * If an error occurs it will instead produce a 'capture-logs-error' event. + * We send a capture-logs-start events to notify the system app and the user, + * since dumping can be a bit long sometimes. + */ + +/* enable Mozilla javascript extensions and global strictness declaration, + * disable valid this checking */ +/* jshint moz: true, esnext: true */ +/* jshint -W097 */ +/* jshint -W040 */ +/* global Services, Components, dump, LogCapture, LogParser, + OS, Promise, volumeService, XPCOMUtils, SystemAppProxy */ + +"use strict"; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +// Constants for creating zip file taken from toolkit/webapps/tests/head.js +const PR_RDWR = 0x04; +const PR_CREATE_FILE = 0x08; +const PR_TRUNCATE = 0x20; + +XPCOMUtils.defineLazyModuleGetter(this, "LogCapture", "resource://gre/modules/LogCapture.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "LogParser", "resource://gre/modules/LogParser.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/Promise.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy", "resource://gre/modules/SystemAppProxy.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "powerManagerService", + "@mozilla.org/power/powermanagerservice;1", + "nsIPowerManagerService"); + +XPCOMUtils.defineLazyServiceGetter(this, "volumeService", + "@mozilla.org/telephony/volume-service;1", + "nsIVolumeService"); + +this.EXPORTED_SYMBOLS = ["LogShake"]; + +function debug(msg) { + dump("LogShake.jsm: "+msg+"\n"); +} + +/** + * An empirically determined amount of acceleration corresponding to a + * shake. + */ +const EXCITEMENT_THRESHOLD = 500; +/** + * The maximum fraction to update the excitement value per frame. This + * corresponds to requiring shaking for approximately 10 motion events (1.6 + * seconds) + */ +const EXCITEMENT_FILTER_ALPHA = 0.2; +const DEVICE_MOTION_EVENT = "devicemotion"; +const SCREEN_CHANGE_EVENT = "screenchange"; +const CAPTURE_LOGS_CONTENT_EVENT = "requestSystemLogs"; +const CAPTURE_LOGS_START_EVENT = "capture-logs-start"; +const CAPTURE_LOGS_ERROR_EVENT = "capture-logs-error"; +const CAPTURE_LOGS_SUCCESS_EVENT = "capture-logs-success"; + +var LogShake = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), + + /** + * If LogShake is in QA Mode, which bundles all files into a compressed archive + */ + qaModeEnabled: false, + + /** + * If LogShake is listening for device motion events. Required due to lag + * between HAL layer of device motion events and listening for device motion + * events. + */ + deviceMotionEnabled: false, + + /** + * We only listen to motion events when the screen is enabled, keep track + * of its state. + */ + screenEnabled: true, + + /** + * Flag monitoring if the preference to enable shake to capture is + * enabled in gaia. + */ + listenToDeviceMotion: true, + + /** + * If a capture has been requested and is waiting for reads/parsing. Used for + * debouncing. + */ + captureRequested: false, + + /** + * The current excitement (movement) level + */ + excitement: 0, + + /** + * Map of files which have log-type information to their parsers + */ + LOGS_WITH_PARSERS: { + "/dev/log/main": LogParser.prettyPrintLogArray, + "/dev/log/system": LogParser.prettyPrintLogArray, + "/dev/log/radio": LogParser.prettyPrintLogArray, + "/dev/log/events": LogParser.prettyPrintLogArray, + "/proc/cmdline": LogParser.prettyPrintArray, + "/proc/kmsg": LogParser.prettyPrintArray, + "/proc/last_kmsg": LogParser.prettyPrintArray, + "/proc/meminfo": LogParser.prettyPrintArray, + "/proc/uptime": LogParser.prettyPrintArray, + "/proc/version": LogParser.prettyPrintArray, + "/proc/vmallocinfo": LogParser.prettyPrintArray, + "/proc/vmstat": LogParser.prettyPrintArray, + "/system/b2g/application.ini": LogParser.prettyPrintArray, + "/cache/recovery/last_install": LogParser.prettyPrintArray, + "/cache/recovery/last_kmsg": LogParser.prettyPrintArray, + "/cache/recovery/last_log": LogParser.prettyPrintArray + }, + + /** + * Start existing, observing motion events if the screen is turned on. + */ + init: function() { + // TODO: no way of querying screen state from power manager + // this.handleScreenChangeEvent({ detail: { + // screenEnabled: powerManagerService.screenEnabled + // }}); + + // However, the screen is always on when we are being enabled because it is + // either due to the phone starting up or a user enabling us directly. + this.handleScreenChangeEvent({ detail: { + screenEnabled: true + }}); + + // Reset excitement to clear residual motion + this.excitement = 0; + + SystemAppProxy.addEventListener(CAPTURE_LOGS_CONTENT_EVENT, this, false); + SystemAppProxy.addEventListener(SCREEN_CHANGE_EVENT, this, false); + + Services.obs.addObserver(this, "xpcom-shutdown", false); + }, + + /** + * Handle an arbitrary event, passing it along to the proper function + */ + handleEvent: function(event) { + switch (event.type) { + case DEVICE_MOTION_EVENT: + if (!this.deviceMotionEnabled) { + return; + } + this.handleDeviceMotionEvent(event); + break; + + case SCREEN_CHANGE_EVENT: + this.handleScreenChangeEvent(event); + break; + + case CAPTURE_LOGS_CONTENT_EVENT: + this.startCapture(); + break; + } + }, + + /** + * Handle an observation from Services.obs + */ + observe: function(subject, topic) { + if (topic === "xpcom-shutdown") { + this.uninit(); + } + }, + + enableQAMode: function() { + debug("Enabling QA Mode"); + this.qaModeEnabled = true; + }, + + disableQAMode: function() { + debug("Disabling QA Mode"); + this.qaModeEnabled = false; + }, + + enableDeviceMotionListener: function() { + this.listenToDeviceMotion = true; + this.startDeviceMotionListener(); + }, + + disableDeviceMotionListener: function() { + this.listenToDeviceMotion = false; + this.stopDeviceMotionListener(); + }, + + startDeviceMotionListener: function() { + if (!this.deviceMotionEnabled && + this.listenToDeviceMotion && + this.screenEnabled) { + SystemAppProxy.addEventListener(DEVICE_MOTION_EVENT, this, false); + this.deviceMotionEnabled = true; + } + }, + + stopDeviceMotionListener: function() { + SystemAppProxy.removeEventListener(DEVICE_MOTION_EVENT, this, false); + this.deviceMotionEnabled = false; + }, + + /** + * Handle a motion event, keeping track of "excitement", the magnitude + * of the device"s acceleration. + */ + handleDeviceMotionEvent: function(event) { + // There is a lag between disabling the event listener and event arrival + // ceasing. + if (!this.deviceMotionEnabled) { + return; + } + + let acc = event.accelerationIncludingGravity; + + // Updates excitement by a factor of at most alpha, ignoring sudden device + // motion. See bug #1101994 for more information. + let newExcitement = acc.x * acc.x + acc.y * acc.y + acc.z * acc.z; + this.excitement += (newExcitement - this.excitement) * EXCITEMENT_FILTER_ALPHA; + + if (this.excitement > EXCITEMENT_THRESHOLD) { + this.startCapture(); + } + }, + + startCapture: function() { + if (this.captureRequested) { + return; + } + this.captureRequested = true; + SystemAppProxy._sendCustomEvent(CAPTURE_LOGS_START_EVENT, {}); + this.captureLogs().then(logResults => { + // On resolution send the success event to the requester + SystemAppProxy._sendCustomEvent(CAPTURE_LOGS_SUCCESS_EVENT, { + logPaths: logResults.logPaths, + logFilenames: logResults.logFilenames + }); + this.captureRequested = false; + }, error => { + // On an error send the error event + SystemAppProxy._sendCustomEvent(CAPTURE_LOGS_ERROR_EVENT, {error: error}); + this.captureRequested = false; + }); + }, + + handleScreenChangeEvent: function(event) { + this.screenEnabled = event.detail.screenEnabled; + if (this.screenEnabled) { + this.startDeviceMotionListener(); + } else { + this.stopDeviceMotionListener(); + } + }, + + /** + * Captures and saves the current device logs, returning a promise that will + * resolve to an array of log filenames. + */ + captureLogs: function() { + return this.readLogs().then(logArrays => { + return this.saveLogs(logArrays); + }); + }, + + /** + * Read in all log files, returning their formatted contents + * @return {Promise<Array>} + */ + readLogs: function() { + let logArrays = {}; + let readPromises = []; + + try { + logArrays["properties"] = + LogParser.prettyPrintPropertiesArray(LogCapture.readProperties()); + } catch (ex) { + Cu.reportError("Unable to get device properties: " + ex); + } + + // Let Gecko perfom the dump to a file, and just collect it + let readAboutMemoryPromise = new Promise(resolve => { + // Wrap the readAboutMemory promise to make it infallible + LogCapture.readAboutMemory().then(aboutMemory => { + let file = OS.Path.basename(aboutMemory); + let logArray; + try { + logArray = LogCapture.readLogFile(aboutMemory); + if (!logArray) { + debug("LogCapture.readLogFile() returned nothing for about:memory"); + } + // We need to remove the dumped file, now that we have it in memory + OS.File.remove(aboutMemory); + } catch (ex) { + Cu.reportError("Unable to handle about:memory dump: " + ex); + } + logArrays[file] = LogParser.prettyPrintArray(logArray); + resolve(); + }, ex => { + Cu.reportError("Unable to get about:memory dump: " + ex); + resolve(); + }); + }); + readPromises.push(readAboutMemoryPromise); + + // Wrap the promise to make it infallible + let readScreenshotPromise = new Promise(resolve => { + LogCapture.getScreenshot().then(screenshot => { + logArrays["screenshot.png"] = screenshot; + resolve(); + }, ex => { + Cu.reportError("Unable to get screenshot dump: " + ex); + resolve(); + }); + }); + readPromises.push(readScreenshotPromise); + + for (let loc in this.LOGS_WITH_PARSERS) { + let logArray; + try { + logArray = LogCapture.readLogFile(loc); + if (!logArray) { + debug("LogCapture.readLogFile() returned nothing for: " + loc); + continue; + } + } catch (ex) { + Cu.reportError("Unable to LogCapture.readLogFile('" + loc + "'): " + ex); + continue; + } + + try { + logArrays[loc] = this.LOGS_WITH_PARSERS[loc](logArray); + } catch (ex) { + Cu.reportError("Unable to parse content of '" + loc + "': " + ex); + continue; + } + } + + // Because the promises we depend upon can't fail this means that the + // blocking log reads will always be honored. + return Promise.all(readPromises).then(() => { + return logArrays; + }); + }, + + /** + * Save the formatted arrays of log files to an sdcard if available + */ + saveLogs: function(logArrays) { + if (!logArrays || Object.keys(logArrays).length === 0) { + return Promise.reject("Zero logs saved"); + } + + if (this.qaModeEnabled) { + return makeBaseLogsDirectory().then(writeLogArchive(logArrays), + rejectFunction("Error making base log directory")); + } else { + return makeBaseLogsDirectory().then(makeLogsDirectory, + rejectFunction("Error making base log directory")) + .then(writeLogFiles(logArrays), + rejectFunction("Error creating log directory")); + } + }, + + /** + * Stop logshake, removing all listeners + */ + uninit: function() { + this.stopDeviceMotionListener(); + SystemAppProxy.removeEventListener(SCREEN_CHANGE_EVENT, this, false); + Services.obs.removeObserver(this, "xpcom-shutdown"); + } +}; + +function getLogFilename(logLocation) { + // sanitize the log location + let logName = logLocation.replace(/\//g, "-"); + if (logName[0] === "-") { + logName = logName.substring(1); + } + + // If no extension is provided, default to forcing .log + let extension = ".log"; + let logLocationExt = logLocation.split("."); + if (logLocationExt.length > 1) { + // otherwise, just append nothing + extension = ""; + } + + return logName + extension; +} + +function getSdcardPrefix() { + return volumeService.getVolumeByName("sdcard").mountPoint; +} + +function getLogDirectoryRoot() { + return "logs"; +} + +function getLogIdentifier() { + let d = new Date(); + d = new Date(d.getTime() - d.getTimezoneOffset() * 60000); + let timestamp = d.toISOString().slice(0, -5).replace(/[:T]/g, "-"); + return timestamp; +} + +function rejectFunction(message) { + return function(err) { + debug(message + ": " + err); + return Promise.reject(err); + }; +} + +function makeBaseLogsDirectory() { + let sdcardPrefix; + try { + sdcardPrefix = getSdcardPrefix(); + } catch(e) { + // Handles missing sdcard + return Promise.reject(e); + } + + let dirNameRoot = getLogDirectoryRoot(); + + let logsRoot = OS.Path.join(sdcardPrefix, dirNameRoot); + + debug("Creating base log directory at root " + sdcardPrefix); + + return OS.File.makeDir(logsRoot, {from: sdcardPrefix}).then( + function() { + return { + sdcardPrefix: sdcardPrefix, + basePrefix: dirNameRoot + }; + } + ); +} + +function makeLogsDirectory({sdcardPrefix, basePrefix}) { + let dirName = getLogIdentifier(); + + let logsRoot = OS.Path.join(sdcardPrefix, basePrefix); + let logsDir = OS.Path.join(logsRoot, dirName); + + debug("Creating base log directory at root " + sdcardPrefix); + debug("Final created directory will be " + logsDir); + + return OS.File.makeDir(logsDir, {ignoreExisting: false}).then( + function() { + debug("Created: " + logsDir); + return { + logPrefix: OS.Path.join(basePrefix, dirName), + sdcardPrefix: sdcardPrefix + }; + }, + rejectFunction("Error at OS.File.makeDir for " + logsDir) + ); +} + +function getFile(filename) { + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(filename); + return file; +} + +/** + * Make a zip file + * @param {String} absoluteZipFilename - Fully qualified desired location of the zip file + * @param {Map<String, Uint8Array>} logArrays - Map from log location to log data + * @return {Array<String>} Paths of entries in the archive + */ +function makeZipFile(absoluteZipFilename, logArrays) { + let logFilenames = []; + let zipWriter = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter); + let zipFile = getFile(absoluteZipFilename); + zipWriter.open(zipFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); + + for (let logLocation in logArrays) { + let logArray = logArrays[logLocation]; + let logFilename = getLogFilename(logLocation); + logFilenames.push(logFilename); + + debug("Adding " + logFilename + " to the zip"); + let logArrayStream = Cc["@mozilla.org/io/arraybuffer-input-stream;1"] + .createInstance(Ci.nsIArrayBufferInputStream); + // Set data to be copied, default offset to 0 because it is not present on + // ArrayBuffer objects + logArrayStream.setData(logArray.buffer, logArray.byteOffset || 0, + logArray.byteLength); + + zipWriter.addEntryStream(logFilename, Date.now(), + Ci.nsIZipWriter.COMPRESSION_DEFAULT, + logArrayStream, false); + } + zipWriter.close(); + + return logFilenames; +} + +function writeLogArchive(logArrays) { + return function({sdcardPrefix, basePrefix}) { + // Now the directory is guaranteed to exist, save the logs into their + // archive file + + let zipFilename = getLogIdentifier() + "-logs.zip"; + let zipPath = OS.Path.join(basePrefix, zipFilename); + let zipPrefix = OS.Path.dirname(zipPath); + let absoluteZipPath = OS.Path.join(sdcardPrefix, zipPath); + + debug("Creating zip file at " + zipPath); + let logFilenames = []; + try { + logFilenames = makeZipFile(absoluteZipPath, logArrays); + } catch(e) { + return Promise.reject(e); + } + debug("Zip file created"); + + return { + logFilenames: logFilenames, + logPaths: [zipPath], + compressed: true + }; + }; +} + +function writeLogFiles(logArrays) { + return function({sdcardPrefix, logPrefix}) { + // Now the directory is guaranteed to exist, save the logs + let logFilenames = []; + let logPaths = []; + let saveRequests = []; + + for (let logLocation in logArrays) { + debug("Requesting save of " + logLocation); + let logArray = logArrays[logLocation]; + let logFilename = getLogFilename(logLocation); + // The local pathrepresents the relative path within the SD card, not the + // absolute path because Gaia will refer to it using the DeviceStorage + // API + let localPath = OS.Path.join(logPrefix, logFilename); + + logFilenames.push(logFilename); + logPaths.push(localPath); + + let absolutePath = OS.Path.join(sdcardPrefix, localPath); + let saveRequest = OS.File.writeAtomic(absolutePath, logArray); + saveRequests.push(saveRequest); + } + + return Promise.all(saveRequests).then( + function() { + return { + logFilenames: logFilenames, + logPaths: logPaths, + compressed: false + }; + }, + rejectFunction("Error at some save request") + ); + }; +} + +LogShake.init(); +this.LogShake = LogShake; diff --git a/b2g/components/MailtoProtocolHandler.js b/b2g/components/MailtoProtocolHandler.js new file mode 100644 index 000000000..500fb9112 --- /dev/null +++ b/b2g/components/MailtoProtocolHandler.js @@ -0,0 +1,46 @@ +/* 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, results: Cr} = Components; + +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +Cu.import('resource://gre/modules/ActivityChannel.jsm'); + +function MailtoProtocolHandler() { +} + +MailtoProtocolHandler.prototype = { + + scheme: "mailto", + defaultPort: -1, + protocolFlags: Ci.nsIProtocolHandler.URI_NORELATIVE | + Ci.nsIProtocolHandler.URI_NOAUTH | + Ci.nsIProtocolHandler.URI_LOADABLE_BY_ANYONE | + Ci.nsIProtocolHandler.URI_DOES_NOT_RETURN_DATA, + allowPort: () => false, + + newURI: function Proto_newURI(aSpec, aOriginCharset) { + let uri = Cc["@mozilla.org/network/simple-uri;1"].createInstance(Ci.nsIURI); + uri.spec = aSpec; + return uri; + }, + + newChannel2: function Proto_newChannel2(aURI, aLoadInfo) { + return new ActivityChannel(aURI, aLoadInfo, + "mail-handler", + { URI: aURI.spec, + type: "mail" }); + }, + + newChannel: function Proto_newChannel(aURI) { + return this.newChannel2(aURI, null); + }, + + classID: Components.ID("{50777e53-0331-4366-a191-900999be386c}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolHandler]) +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MailtoProtocolHandler]); diff --git a/b2g/components/OMAContentHandler.js b/b2g/components/OMAContentHandler.js new file mode 100644 index 000000000..56c87a3b2 --- /dev/null +++ b/b2g/components/OMAContentHandler.js @@ -0,0 +1,57 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +"use strict"; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyGetter(this, "cpmm", function() { + return Cc["@mozilla.org/childprocessmessagemanager;1"] + .getService(Ci.nsIMessageSender); +}); + +function debug(aMsg) { + //dump("--*-- OMAContentHandler: " + aMsg + "\n"); +} + +const NS_ERROR_WONT_HANDLE_CONTENT = 0x805d0001; + +function OMAContentHandler() { +} + +OMAContentHandler.prototype = { + classID: Components.ID("{a6b2ab13-9037-423a-9897-dde1081be323}"), + + _xpcom_factory: { + createInstance: function createInstance(outer, iid) { + if (outer != null) { + throw Cr.NS_ERROR_NO_AGGREGATION; + } + return new OMAContentHandler().QueryInterface(iid); + } + }, + + handleContent: function handleContent(aMimetype, aContext, aRequest) { + if (!(aRequest instanceof Ci.nsIChannel)) { + throw NS_ERROR_WONT_HANDLE_CONTENT; + } + + let detail = { + "type": aMimetype, + "url": aRequest.URI.spec + }; + cpmm.sendAsyncMessage("content-handler", detail); + + aRequest.cancel(Cr.NS_BINDING_ABORTED); + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentHandler]) +} + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([OMAContentHandler]); diff --git a/b2g/components/OopCommandLine.js b/b2g/components/OopCommandLine.js new file mode 100644 index 000000000..658bbdde5 --- /dev/null +++ b/b2g/components/OopCommandLine.js @@ -0,0 +1,46 @@ +/* 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); + +function oopCommandlineHandler() { +} + +oopCommandlineHandler.prototype = { + handle: function(cmdLine) { + let oopFlag = cmdLine.handleFlag("oop", false); + if (oopFlag) { + /** + * Manipulate preferences by adding to the *default* branch. Adding + * to the default branch means the changes we make won"t get written + * back to user preferences. + */ + let prefs = Services.prefs + let branch = prefs.getDefaultBranch(""); + + try { + // Turn on all OOP services, making desktop run similar to phone + // environment + branch.setBoolPref("dom.ipc.tabs.disabled", false); + branch.setBoolPref("layers.acceleration.disabled", false); + branch.setBoolPref("layers.offmainthreadcomposition.async-animations", true); + branch.setBoolPref("layers.async-video.enabled", true); + branch.setBoolPref("layers.async-pan-zoom.enabled", true); + branch.setCharPref("gfx.content.azure.backends", "cairo"); + } catch (e) { } + + } + if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) { + cmdLine.preventDefault = true; + } + }, + + helpInfo: " --oop Use out-of-process model in B2G\n", + classID: Components.ID("{e30b0e13-2d12-4cb0-bc4c-4e617a1bf76e}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsICommandLineHandler]), +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([oopCommandlineHandler]); diff --git a/b2g/components/OrientationChangeHandler.jsm b/b2g/components/OrientationChangeHandler.jsm new file mode 100644 index 000000000..5007b70e0 --- /dev/null +++ b/b2g/components/OrientationChangeHandler.jsm @@ -0,0 +1,70 @@ +/* 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 Cu = Components.utils; +Cu.import("resource://gre/modules/Services.jsm"); + +var window = Services.wm.getMostRecentWindow("navigator:browser"); +var system = window.document.getElementById("systemapp"); + +var OrientationChangeHandler = { + // Clockwise orientations, looping + orientations: ["portrait-primary", "landscape-secondary", + "portrait-secondary", "landscape-primary", + "portrait-primary"], + + lastOrientation: "portrait-primary", + + init: function() { + window.screen.addEventListener("mozorientationchange", this, true); + }, + + handleEvent: function(evt) { + let newOrientation = window.screen.mozOrientation; + let orientationIndex = this.orientations.indexOf(this.lastOrientation); + let nextClockwiseOrientation = this.orientations[orientationIndex + 1]; + let fullSwitch = (newOrientation.split("-")[0] == + this.lastOrientation.split("-")[0]); + + this.lastOrientation = newOrientation; + + let angle, xFactor, yFactor; + if (fullSwitch) { + angle = 180; + xFactor = 1; + } else { + angle = (nextClockwiseOrientation == newOrientation) ? 90 : -90; + xFactor = window.innerWidth / window.innerHeight; + } + yFactor = 1 / xFactor; + + system.style.transition = ""; + system.style.transform = "rotate(" + angle + "deg)" + + "scale(" + xFactor + ", " + yFactor + ")"; + + function trigger() { + system.style.transition = "transform .25s cubic-bezier(.15, .7, .6, .9)"; + + system.style.opacity = ""; + system.style.transform = ""; + } + + // 180deg rotation, no resize + if (fullSwitch) { + window.setTimeout(trigger); + return; + } + + window.addEventListener("resize", function waitForResize(e) { + window.removeEventListener("resize", waitForResize); + trigger(); + }); + } +}; + +OrientationChangeHandler.init(); diff --git a/b2g/components/PresentationRequestUIGlue.js b/b2g/components/PresentationRequestUIGlue.js new file mode 100644 index 000000000..5c50401de --- /dev/null +++ b/b2g/components/PresentationRequestUIGlue.js @@ -0,0 +1,116 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=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" + +function debug(aMsg) { + // dump("-*- PresentationRequestUIGlue: " + aMsg + "\n"); +} + +const { interfaces: Ci, utils: Cu, classes: Cc } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy", + "resource://gre/modules/SystemAppProxy.jsm"); + +function PresentationRequestUIGlue() { } + +PresentationRequestUIGlue.prototype = { + + sendRequest: function(aUrl, aSessionId, aDevice) { + let localDevice; + try { + localDevice = aDevice.QueryInterface(Ci.nsIPresentationLocalDevice); + } catch (e) {} + + if (localDevice) { + return this.sendTo1UA(aUrl, aSessionId, localDevice.windowId); + } else { + return this.sendTo2UA(aUrl, aSessionId); + } + }, + + // For 1-UA scenario + sendTo1UA: function(aUrl, aSessionId, aWindowId) { + return new Promise((aResolve, aReject) => { + let handler = (evt) => { + if (evt.type === "unload") { + SystemAppProxy.removeEventListenerWithId(aWindowId, + "unload", + handler); + SystemAppProxy.removeEventListenerWithId(aWindowId, + "mozPresentationContentEvent", + handler); + aReject(); + } + if (evt.type === "mozPresentationContentEvent" && + evt.detail.id == aSessionId) { + SystemAppProxy.removeEventListenerWithId(aWindowId, + "unload", + handler); + SystemAppProxy.removeEventListenerWithId(aWindowId, + "mozPresentationContentEvent", + handler); + this.appLaunchCallback(evt.detail, aResolve, aReject); + } + }; + // If system(-remote) app is closed. + SystemAppProxy.addEventListenerWithId(aWindowId, + "unload", + handler); + // Listen to the result for the opened iframe from front-end. + SystemAppProxy.addEventListenerWithId(aWindowId, + "mozPresentationContentEvent", + handler); + SystemAppProxy.sendCustomEventWithId(aWindowId, + "mozPresentationChromeEvent", + { type: "presentation-launch-receiver", + url: aUrl, + id: aSessionId }); + }); + }, + + // For 2-UA scenario + sendTo2UA: function(aUrl, aSessionId) { + return new Promise((aResolve, aReject) => { + let handler = (evt) => { + if (evt.type === "mozPresentationContentEvent" && + evt.detail.id == aSessionId) { + SystemAppProxy.removeEventListener("mozPresentationContentEvent", + handler); + this.appLaunchCallback(evt.detail, aResolve, aReject); + } + }; + + // Listen to the result for the opened iframe from front-end. + SystemAppProxy.addEventListener("mozPresentationContentEvent", + handler); + SystemAppProxy._sendCustomEvent("mozPresentationChromeEvent", + { type: "presentation-launch-receiver", + url: aUrl, + id: aSessionId }); + }); + }, + + appLaunchCallback: function(aDetail, aResolve, aReject) { + switch(aDetail.type) { + case "presentation-receiver-launched": + aResolve(aDetail.frame); + break; + case "presentation-receiver-permission-denied": + aReject(); + break; + } + }, + + classID: Components.ID("{ccc8a839-0b64-422b-8a60-fb2af0e376d0}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationRequestUIGlue]) +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PresentationRequestUIGlue]); diff --git a/b2g/components/ProcessGlobal.js b/b2g/components/ProcessGlobal.js new file mode 100644 index 000000000..94326ad50 --- /dev/null +++ b/b2g/components/ProcessGlobal.js @@ -0,0 +1,202 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* 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 code exists to be a "grab bag" of global code that needs to be + * loaded per B2G process, but doesn't need to directly interact with + * web content. + * + * (It's written as an XPCOM service because it needs to watch + * app-startup.) + */ + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import('resource://gre/modules/Services.jsm'); +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); + +XPCOMUtils.defineLazyServiceGetter(this, "settings", + "@mozilla.org/settingsService;1", + "nsISettingsService"); + +function debug(msg) { + log(msg); +} +function log(msg) { + // This file implements console.log(), so use dump(). + //dump('ProcessGlobal: ' + msg + '\n'); +} + +function formatStackFrame(aFrame) { + let functionName = aFrame.functionName || '<anonymous>'; + return ' at ' + functionName + + ' (' + aFrame.filename + ':' + aFrame.lineNumber + + ':' + aFrame.columnNumber + ')'; +} + +function ConsoleMessage(aMsg, aLevel) { + this.timeStamp = Date.now(); + this.msg = aMsg; + + switch (aLevel) { + case 'error': + case 'assert': + this.logLevel = Ci.nsIConsoleMessage.error; + break; + case 'warn': + this.logLevel = Ci.nsIConsoleMessage.warn; + break; + case 'log': + case 'info': + this.logLevel = Ci.nsIConsoleMessage.info; + break; + default: + this.logLevel = Ci.nsIConsoleMessage.debug; + break; + } +} + +function toggleUnrestrictedDevtools(unrestricted) { + Services.prefs.setBoolPref("devtools.debugger.forbid-certified-apps", + !unrestricted); + Services.prefs.setBoolPref("dom.apps.developer_mode", unrestricted); + // TODO: Remove once bug 1125916 is fixed. + Services.prefs.setBoolPref("network.disable.ipc.security", unrestricted); + Services.prefs.setBoolPref("dom.webcomponents.enabled", unrestricted); + let lock = settings.createLock(); + lock.set("developer.menu.enabled", unrestricted, null); + lock.set("devtools.unrestricted", unrestricted, null); +} + +ConsoleMessage.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIConsoleMessage]), + toString: function() { return this.msg; } +}; + +const gFactoryResetFile = "__post_reset_cmd__"; + +function ProcessGlobal() {} +ProcessGlobal.prototype = { + classID: Components.ID('{1a94c87a-5ece-4d11-91e1-d29c29f21b28}'), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + + wipeDir: function(path) { + log("wipeDir " + path); + let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + dir.initWithPath(path); + if (!dir.exists() || !dir.isDirectory()) { + return; + } + let entries = dir.directoryEntries; + while (entries.hasMoreElements()) { + let file = entries.getNext().QueryInterface(Ci.nsIFile); + log("Deleting " + file.path); + try { + file.remove(true); + } catch(e) {} + } + }, + + processCommandsFile: function(text) { + log("processCommandsFile " + text); + let lines = text.split("\n"); + lines.forEach((line) => { + log(line); + let params = line.split(" "); + switch (params[0]) { + case "root": + log("unrestrict devtools"); + toggleUnrestrictedDevtools(true); + break; + case "wipe": + this.wipeDir(params[1]); + case "normal": + log("restrict devtools"); + toggleUnrestrictedDevtools(false); + break; + } + }); + }, + + cleanupAfterFactoryReset: function() { + log("cleanupAfterWipe start"); + + Cu.import("resource://gre/modules/osfile.jsm"); + let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + dir.initWithPath("/persist"); + var postResetFile = dir.exists() ? + OS.Path.join("/persist", gFactoryResetFile): + OS.Path.join("/cache", gFactoryResetFile); + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(postResetFile); + if (!file.exists()) { + debug("No additional command.") + return; + } + + let promise = OS.File.read(postResetFile); + promise.then( + (array) => { + file.remove(false); + let decoder = new TextDecoder(); + this.processCommandsFile(decoder.decode(array)); + }, + function onError(error) { + debug("Error: " + error); + } + ); + + log("cleanupAfterWipe end."); + }, + + observe: function pg_observe(subject, topic, data) { + switch (topic) { + case 'app-startup': { + Services.obs.addObserver(this, 'console-api-log-event', false); + let inParent = Cc["@mozilla.org/xre/app-info;1"] + .getService(Ci.nsIXULRuntime) + .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT; + if (inParent) { + Services.ppmm.addMessageListener("getProfD", function(message) { + return Services.dirsvc.get("ProfD", Ci.nsIFile).path; + }); + + this.cleanupAfterFactoryReset(); + } + break; + } + case 'console-api-log-event': { + // Pipe `console` log messages to the nsIConsoleService which + // writes them to logcat on Gonk. + let message = subject.wrappedJSObject; + let args = message.arguments; + let stackTrace = ''; + + if (message.stacktrace && + (message.level == 'assert' || message.level == 'error' || message.level == 'trace')) { + stackTrace = Array.map(message.stacktrace, formatStackFrame).join('\n'); + } else { + stackTrace = formatStackFrame(message); + } + + if (stackTrace) { + args.push('\n' + stackTrace); + } + + let msg = 'Content JS ' + message.level.toUpperCase() + ': ' + Array.join(args, ' '); + Services.console.logMessage(new ConsoleMessage(msg, message.level)); + break; + } + } + }, +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ProcessGlobal]); diff --git a/b2g/components/RecoveryService.js b/b2g/components/RecoveryService.js new file mode 100644 index 000000000..493763e6d --- /dev/null +++ b/b2g/components/RecoveryService.js @@ -0,0 +1,160 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* 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, results: Cr} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/ctypes.jsm"); +Cu.import("resource://gre/modules/AppConstants.jsm"); + +const RECOVERYSERVICE_CID = Components.ID("{b3caca5d-0bb0-48c6-912b-6be6cbf08832}"); +const RECOVERYSERVICE_CONTRACTID = "@mozilla.org/recovery-service;1"; + +function log(msg) { + dump("-*- RecoveryService: " + msg + "\n"); +} + +const isGonk = AppConstants.platform === 'gonk'; + +if (isGonk) { + var librecovery = (function() { + let library; + try { + library = ctypes.open("librecovery.so"); + } catch (e) { + log("Unable to open librecovery.so"); + throw Cr.NS_ERROR_FAILURE; + } + // Bug 1163956, modify updatePath from ctyps.char.ptr to ctype.char.array(4096) + // align with librecovery.h. 4096 comes from PATH_MAX + let FotaUpdateStatus = new ctypes.StructType("FotaUpdateStatus", [ + { result: ctypes.int }, + { updatePath: ctypes.char.array(4096) } + ]); + + return { + factoryReset: library.declare("factoryReset", + ctypes.default_abi, + ctypes.int), + installFotaUpdate: library.declare("installFotaUpdate", + ctypes.default_abi, + ctypes.int, + ctypes.char.ptr, + ctypes.int), + + FotaUpdateStatus: FotaUpdateStatus, + getFotaUpdateStatus: library.declare("getFotaUpdateStatus", + ctypes.default_abi, + ctypes.int, + FotaUpdateStatus.ptr) + }; + })(); + +} + +const gFactoryResetFile = "__post_reset_cmd__"; + +function RecoveryService() {} + +RecoveryService.prototype = { + classID: RECOVERYSERVICE_CID, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIRecoveryService]), + classInfo: XPCOMUtils.generateCI({ + classID: RECOVERYSERVICE_CID, + contractID: RECOVERYSERVICE_CONTRACTID, + interfaces: [Ci.nsIRecoveryService], + classDescription: "B2G Recovery Service" + }), + + factoryReset: function RS_factoryReset(reason) { + if (!isGonk) { + Cr.NS_ERROR_FAILURE; + } + + function doReset() { + // If this succeeds, then the device reboots and this never returns + if (librecovery.factoryReset() != 0) { + log("Error: Factory reset failed. Trying again after clearing cache."); + } + let cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"] + .getService(Ci.nsICacheStorageService); + cache.clear(); + if (librecovery.factoryReset() != 0) { + log("Error: Factory reset failed again"); + } + } + + log("factoryReset " + reason); + let commands = []; + if (reason == "wipe") { + let volumeService = Cc["@mozilla.org/telephony/volume-service;1"] + .getService(Ci.nsIVolumeService); + let volNames = volumeService.getVolumeNames(); + log("Found " + volNames.length + " volumes"); + + for (let i = 0; i < volNames.length; i++) { + let name = volNames.queryElementAt(i, Ci.nsISupportsString); + let volume = volumeService.getVolumeByName(name.data); + log("Got volume: " + name.data + " at " + volume.mountPoint); + commands.push("wipe " + volume.mountPoint); + } + } else if (reason == "root") { + commands.push("root"); + } + + if (commands.length > 0) { + Cu.import("resource://gre/modules/osfile.jsm"); + let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + dir.initWithPath("/persist"); + var postResetFile = dir.exists() ? + OS.Path.join("/persist", gFactoryResetFile): + OS.Path.join("/cache", gFactoryResetFile); + let encoder = new TextEncoder(); + let text = commands.join("\n"); + let array = encoder.encode(text); + let promise = OS.File.writeAtomic(postResetFile, array, + { tmpPath: postResetFile + ".tmp" }); + + promise.then(doReset, function onError(error) { + log("Error: " + error); + }); + } else { + doReset(); + } + }, + + installFotaUpdate: function RS_installFotaUpdate(updatePath) { + if (!isGonk) { + throw Cr.NS_ERROR_FAILURE; + } + + // If this succeeds, then the device reboots and this never returns + if (librecovery.installFotaUpdate(updatePath, updatePath.length) != 0) { + log("Error: FOTA install failed. Trying again after clearing cache."); + } + var cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"].getService(Ci.nsICacheStorageService); + cache.clear(); + if (librecovery.installFotaUpdate(updatePath, updatePath.length) != 0) { + log("Error: FOTA install failed again"); + } + }, + + getFotaUpdateStatus: function RS_getFotaUpdateStatus() { + let status = Ci.nsIRecoveryService.FOTA_UPDATE_UNKNOWN; + + if (isGonk) { + let cStatus = librecovery.FotaUpdateStatus(); + + if (librecovery.getFotaUpdateStatus(cStatus.address()) == 0) { + status = cStatus.result; + } + } + return status; + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([RecoveryService]); diff --git a/b2g/components/SafeMode.jsm b/b2g/components/SafeMode.jsm new file mode 100644 index 000000000..9f9342f67 --- /dev/null +++ b/b2g/components/SafeMode.jsm @@ -0,0 +1,150 @@ +/* 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 = ["SafeMode"]; + +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/AppConstants.jsm"); + +const kSafeModePref = "b2g.safe_mode"; +const kSafeModePage = "safe_mode.html"; + +function debug(aStr) { + //dump("-*- SafeMode: " + aStr + "\n"); +} + +// This module is responsible for checking whether we want to start in safe +// mode or not. The flow is as follow: +// - wait for the `b2g.safe_mode` preference to be set to something different +// than `unset` by nsAppShell +// - If it's set to `no`, just start normally. +// - If it's set to `yes`, we load a stripped down system app from safe_mode.html" +// - This page is responsible to dispatch a mozContentEvent to us. +// - If the user choose SafeMode, we disable all add-ons. +// - We go on with startup. + +this.SafeMode = { + // Returns a promise that resolves when nsAppShell has set the + // b2g.safe_mode_state_ready preference to `true`. + _waitForPref: function() { + debug("waitForPref"); + try { + let currentMode = Services.prefs.getCharPref(kSafeModePref); + debug("current mode: " + currentMode); + if (currentMode !== "unset") { + return Promise.resolve(); + } + } catch(e) { debug("No current mode available!"); } + + // Wait for the preference to toggle. + return new Promise((aResolve, aReject) => { + let observer = function(aSubject, aTopic, aData) { + if (Services.prefs.getCharPref(kSafeModePref)) { + Services.prefs.removeObserver(kSafeModePref, observer, false); + aResolve(); + } + } + + Services.prefs.addObserver(kSafeModePref, observer, false); + }); + }, + + // Resolves once the user has decided how to start. + // Note that all the actions happen here, so there is no other action from + // consumers than to go on. + _waitForUser: function() { + debug("waitForUser"); + let isSafeMode = Services.prefs.getCharPref(kSafeModePref) === "yes"; + if (!isSafeMode) { + return Promise.resolve(); + } + debug("Starting in Safe Mode!"); + + // Load $system_app/safe_mode.html as a full screen iframe, and wait for + // the user to make a choice. + let shell = SafeMode.window.shell; + let document = SafeMode.window.document; + SafeMode.window.screen.mozLockOrientation("portrait"); + + let url = Services.io.newURI(shell.homeURL, null, null) + .resolve(kSafeModePage); + debug("Registry is ready, loading " + url); + let frame = document.createElementNS("http://www.w3.org/1999/xhtml", "html:iframe"); + frame.setAttribute("mozbrowser", "true"); + frame.setAttribute("mozapp", shell.manifestURL); + frame.setAttribute("id", "systemapp"); // To keep screen.js happy. + let contentBrowser = document.body.appendChild(frame); + + return new Promise((aResolve, aReject) => { + let content = contentBrowser.contentWindow; + + // Stripped down version of the system app bootstrap. + function handleEvent(e) { + switch(e.type) { + case "mozbrowserloadstart": + if (content.document.location == "about:blank") { + contentBrowser.addEventListener("mozbrowserlocationchange", handleEvent, true); + contentBrowser.removeEventListener("mozbrowserloadstart", handleEvent, true); + return; + } + + notifyContentStart(); + break; + case "mozbrowserlocationchange": + if (content.document.location == "about:blank") { + return; + } + + contentBrowser.removeEventListener("mozbrowserlocationchange", handleEvent, true); + notifyContentStart(); + break; + case "mozContentEvent": + content.removeEventListener("mozContentEvent", handleEvent, true); + contentBrowser.parentNode.removeChild(contentBrowser); + + if (e.detail == "safemode-yes") { + // Really starting in safe mode, let's disable add-ons first. + // TODO: disable add-ons + aResolve(); + } else { + aResolve(); + } + break; + } + } + + function notifyContentStart() { + let window = SafeMode.window; + window.shell.sendEvent(window, "SafeModeStart"); + contentBrowser.setVisible(true); + + // browser-ui-startup-complete is used by the AppShell to stop the + // boot animation and start gecko rendering. + Services.obs.notifyObservers(null, "browser-ui-startup-complete", ""); + content.addEventListener("mozContentEvent", handleEvent, true); + } + + contentBrowser.addEventListener("mozbrowserloadstart", handleEvent, true); + contentBrowser.src = url; + }); + }, + + // Returns a Promise that resolves once we have decided to run in safe mode + // or not. All the safe mode switching actions happen before resolving the + // promise. + check: function(aWindow) { + debug("check"); + this.window = aWindow; + if (AppConstants.platform !== "gonk") { + // For now we only have gonk support. + return Promise.resolve(); + } + + return this._waitForPref().then(this._waitForUser); + } +} diff --git a/b2g/components/Screenshot.jsm b/b2g/components/Screenshot.jsm new file mode 100644 index 000000000..e6f809375 --- /dev/null +++ b/b2g/components/Screenshot.jsm @@ -0,0 +1,43 @@ +/* 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/XPCOMUtils.jsm'); + +XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy", "resource://gre/modules/SystemAppProxy.jsm"); + +this.EXPORTED_SYMBOLS = ['Screenshot']; + +var Screenshot = { + get: function screenshot_get() { + let systemAppFrame = SystemAppProxy.getFrame(); + let window = systemAppFrame.ownerDocument.defaultView; + let document = window.document; + + var canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas'); + var docRect = document.body.getBoundingClientRect(); + var width = docRect.width; + var height = docRect.height; + + // Convert width and height from CSS pixels (potentially fractional) + // to device pixels (integer). + var scale = window.devicePixelRatio; + canvas.setAttribute('width', Math.round(width * scale)); + canvas.setAttribute('height', Math.round(height * scale)); + + var context = canvas.getContext('2d'); + var flags = + context.DRAWWINDOW_DRAW_CARET | + context.DRAWWINDOW_DRAW_VIEW | + context.DRAWWINDOW_USE_WIDGET_LAYERS; + context.scale(scale, scale); + context.drawWindow(window, 0, 0, width, height, 'rgb(255,255,255)', flags); + + return canvas.mozGetAsFile('screenshot', 'image/png'); + } +}; +this.Screenshot = Screenshot; diff --git a/b2g/components/SignInToWebsite.jsm b/b2g/components/SignInToWebsite.jsm new file mode 100644 index 000000000..fd1349d46 --- /dev/null +++ b/b2g/components/SignInToWebsite.jsm @@ -0,0 +1,444 @@ +/* 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/. */ + +/* + * SignInToWebsite.jsm - UX Controller and means for accessing identity + * cookies on behalf of relying parties. + * + * Currently, the b2g security architecture isolates web applications + * so that each window has access only to a local cookie jar: + * + * To prevent Web apps from interfering with one another, each one is + * hosted on a separate domain, and therefore may only access the + * resources associated with its domain. These resources include + * things such as IndexedDB databases, cookies, offline storage, + * and so forth. + * + * -- https://developer.mozilla.org/en-US/docs/Mozilla/Firefox_OS/Security/Security_model + * + * As a result, an authentication system like Persona cannot share its + * cookie jar with multiple relying parties, and so would require a + * fresh login request in every window. This would not be a good + * experience. + * + * + * In order for navigator.id.request() to maintain state in a single + * cookie jar, we cause all Persona interactions to take place in a + * content context that is launched by the system application, with the + * result that Persona has a single cookie jar that all Relying + * Parties can use. Since of course those Relying Parties cannot + * reach into the system cookie jar, the Controller in this module + * provides a way to get messages and data to and fro between the + * Relying Party in its window context, and the Persona internal api + * in its context. + * + * On the Relying Party's side, say a web page invokes + * navigator.id.watch(), to register callbacks, and then + * navigator.id.request() to request an assertion. The navigator.id + * calls are provided by nsDOMIdentity. nsDOMIdentity messages down + * to the privileged DOMIdentity code (using cpmm and ppmm message + * managers). DOMIdentity stores the state of Relying Party flows + * using an Identity service (MinimalIdentity.jsm), and emits messages + * requesting Persona functions (doWatch, doReady, doLogout). + * + * The Identity service sends these observer messages to the + * Controller in this module, which in turn triggers content to open a + * window to host the Persona js. If user interaction is required, + * content will open the trusty UI. If user interaction is not required, + * and we only need to get to Persona functions, content will open a + * hidden iframe. In either case, a window is opened into which the + * controller causes the script identity.js to be injected. This + * script provides the glue between the in-page javascript and the + * pipe back down to the Controller, translating navigator.internal + * function callbacks into messages sent back to the Controller. + * + * As a result, a navigator.internal function in the hosted popup or + * iframe can call back to the injected identity.js (doReady, doLogin, + * or doLogout). identity.js callbacks send messages back through the + * pipe to the Controller. The controller invokes the corresponding + * function on the Identity Service (doReady, doLogin, or doLogout). + * The IdentityService calls the corresponding callback for the + * correct Relying Party, which causes DOMIdentity to send a message + * up to the Relying Party through nsDOMIdentity + * (Identity:RP:Watch:OnLogin etc.), and finally, nsDOMIdentity + * receives these messages and calls the original callback that the + * Relying Party registered (navigator.id.watch(), + * navigator.id.request(), or navigator.id.logout()). + */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["SignInToWebsiteController"]; + +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "getRandomId", + "resource://gre/modules/identity/IdentityUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "IdentityService", + "resource://gre/modules/identity/MinimalIdentity.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "Logger", + "resource://gre/modules/identity/LogUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy", + "resource://gre/modules/SystemAppProxy.jsm"); + +// The default persona uri; can be overwritten with toolkit.identity.uri pref. +// Do this if you want to repoint to a different service for testing. +// There's no point in setting up an observer to monitor the pref, as b2g prefs +// can only be overwritten when the profie is recreated. So just get the value +// on start-up. +var kPersonaUri = "https://firefoxos.persona.org"; +try { + kPersonaUri = Services.prefs.getCharPref("toolkit.identity.uri"); +} catch(noSuchPref) { + // stick with the default value +} + +// JS shim that contains the callback functions that +// live within the identity UI provisioning frame. +const kIdentityShimFile = "chrome://b2g/content/identity.js"; + +// Type of MozChromeEvents to handle id dialogs. +const kOpenIdentityDialog = "id-dialog-open"; +const kDoneIdentityDialog = "id-dialog-done"; +const kCloseIdentityDialog = "id-dialog-close-iframe"; + +// Observer messages to communicate to shim +const kIdentityDelegateWatch = "identity-delegate-watch"; +const kIdentityDelegateRequest = "identity-delegate-request"; +const kIdentityDelegateLogout = "identity-delegate-logout"; +const kIdentityDelegateFinished = "identity-delegate-finished"; +const kIdentityDelegateReady = "identity-delegate-ready"; + +const kIdentityControllerDoMethod = "identity-controller-doMethod"; + +function log(...aMessageArgs) { + Logger.log.apply(Logger, ["SignInToWebsiteController"].concat(aMessageArgs)); +} + +log("persona uri =", kPersonaUri); + +function sendChromeEvent(details) { + details.uri = kPersonaUri; + SystemAppProxy.dispatchEvent(details); +} + +function Pipe() { + this._watchers = []; +} + +Pipe.prototype = { + init: function pipe_init() { + Services.obs.addObserver(this, "identity-child-process-shutdown", false); + Services.obs.addObserver(this, "identity-controller-unwatch", false); + }, + + uninit: function pipe_uninit() { + Services.obs.removeObserver(this, "identity-child-process-shutdown"); + Services.obs.removeObserver(this, "identity-controller-unwatch"); + }, + + observe: function Pipe_observe(aSubject, aTopic, aData) { + let options = {}; + if (aSubject) { + options = aSubject.wrappedJSObject; + } + switch (aTopic) { + case "identity-child-process-shutdown": + log("pipe removing watchers by message manager"); + this._removeWatchers(null, options.messageManager); + break; + + case "identity-controller-unwatch": + log("unwatching", options.id); + this._removeWatchers(options.id, options.messageManager); + break; + } + }, + + _addWatcher: function Pipe__addWatcher(aId, aMm) { + log("Adding watcher with id", aId); + for (let i = 0; i < this._watchers.length; ++i) { + let watcher = this._watchers[i]; + if (this._watcher.id === aId) { + watcher.count++; + return; + } + } + this._watchers.push({id: aId, count: 1, mm: aMm}); + }, + + _removeWatchers: function Pipe__removeWatcher(aId, aMm) { + let checkId = aId !== null; + let index = -1; + for (let i = 0; i < this._watchers.length; ++i) { + let watcher = this._watchers[i]; + if (watcher.mm === aMm && + (!checkId || (checkId && watcher.id === aId))) { + index = i; + break; + } + } + + if (index !== -1) { + if (checkId) { + if (--(this._watchers[index].count) === 0) { + this._watchers.splice(index, 1); + } + } else { + this._watchers.splice(index, 1); + } + } + + if (this._watchers.length === 0) { + log("No more watchers; clean up persona host iframe"); + let detail = { + type: kCloseIdentityDialog + }; + log('telling content to close the dialog'); + // tell content to close the dialog + sendChromeEvent(detail); + } + }, + + communicate: function(aRpOptions, aContentOptions, aMessageCallback) { + let rpID = aRpOptions.id; + let rpMM = aRpOptions.mm; + if (rpMM) { + this._addWatcher(rpID, rpMM); + } + + log("RP options:", aRpOptions, "\n content options:", aContentOptions); + + // This content variable is injected into the scope of + // kIdentityShimFile, where it is used to access the BrowserID object + // and its internal API. + let mm = null; + let uuid = getRandomId(); + let self = this; + + function removeMessageListeners() { + if (mm) { + mm.removeMessageListener(kIdentityDelegateFinished, identityDelegateFinished); + mm.removeMessageListener(kIdentityControllerDoMethod, aMessageCallback); + } + } + + function identityDelegateFinished() { + removeMessageListeners(); + + let detail = { + type: kDoneIdentityDialog, + showUI: aContentOptions.showUI || false, + id: kDoneIdentityDialog + "-" + uuid, + requestId: aRpOptions.id + }; + log('received delegate finished; telling content to close the dialog'); + sendChromeEvent(detail); + self._removeWatchers(rpID, rpMM); + } + + SystemAppProxy.addEventListener("mozContentEvent", function getAssertion(evt) { + let msg = evt.detail; + if (!msg.id.match(uuid)) { + return; + } + + switch (msg.id) { + case kOpenIdentityDialog + '-' + uuid: + if (msg.type === 'cancel') { + // The user closed the dialog. Clean up and call cancel. + SystemAppProxy.removeEventListener("mozContentEvent", getAssertion); + removeMessageListeners(); + aMessageCallback({json: {method: "cancel"}}); + } else { + // The window has opened. Inject the identity shim file containing + // the callbacks in the content script. This could be either the + // visible popup that the user interacts with, or it could be an + // invisible frame. + let frame = evt.detail.frame; + let frameLoader = frame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader; + mm = frameLoader.messageManager; + try { + mm.loadFrameScript(kIdentityShimFile, true, true); + log("Loaded shim", kIdentityShimFile); + } catch (e) { + log("Error loading", kIdentityShimFile, "as a frame script:", e); + } + + // There are two messages that the delegate can send back: a "do + // method" event, and a "finished" event. We pass the do-method + // events straight to the caller for interpretation and handling. + // If we receive a "finished" event, then the delegate is done, so + // we shut down the pipe and clean up. + mm.addMessageListener(kIdentityControllerDoMethod, aMessageCallback); + mm.addMessageListener(kIdentityDelegateFinished, identityDelegateFinished); + + mm.sendAsyncMessage(aContentOptions.message, aRpOptions); + } + break; + + case kDoneIdentityDialog + '-' + uuid: + // Received our assertion. The message manager callbacks will handle + // communicating back to the IDService. All we have to do is remove + // this listener. + SystemAppProxy.removeEventListener("mozContentEvent", getAssertion); + break; + + default: + log("ERROR - Unexpected message: id=" + msg.id + ", type=" + msg.type + ", errorMsg=" + msg.errorMsg); + break; + } + + }); + + // Tell content to open the identity iframe or trusty popup. The parameter + // showUI signals whether user interaction is needed. If it is, content will + // open a dialog; if not, a hidden iframe. In each case, BrowserID is + // available in the context. + let detail = { + type: kOpenIdentityDialog, + showUI: aContentOptions.showUI || false, + id: kOpenIdentityDialog + "-" + uuid, + requestId: aRpOptions.id + }; + + sendChromeEvent(detail); + } + +}; + +/* + * The controller sits between the IdentityService used by DOMIdentity + * and a content process launches an (invisible) iframe or (visible) + * trusty UI. Using an injected js script (identity.js), the + * controller enables the content window to access the persona identity + * storage in the system cookie jar and send events back via the + * controller into IdentityService and DOM, and ultimately up to the + * Relying Party, which is open in a different window context. + */ +this.SignInToWebsiteController = { + + /* + * Initialize the controller. To use a different content communication pipe, + * such as when mocking it in tests, pass aOptions.pipe. + */ + init: function SignInToWebsiteController_init(aOptions) { + aOptions = aOptions || {}; + this.pipe = aOptions.pipe || new Pipe(); + Services.obs.addObserver(this, "identity-controller-watch", false); + Services.obs.addObserver(this, "identity-controller-request", false); + Services.obs.addObserver(this, "identity-controller-logout", false); + }, + + uninit: function SignInToWebsiteController_uninit() { + Services.obs.removeObserver(this, "identity-controller-watch"); + Services.obs.removeObserver(this, "identity-controller-request"); + Services.obs.removeObserver(this, "identity-controller-logout"); + }, + + observe: function SignInToWebsiteController_observe(aSubject, aTopic, aData) { + log("observe: received", aTopic, "with", aData, "for", aSubject); + let options = null; + if (aSubject) { + options = aSubject.wrappedJSObject; + } + switch (aTopic) { + case "identity-controller-watch": + this.doWatch(options); + break; + case "identity-controller-request": + this.doRequest(options); + break; + case "identity-controller-logout": + this.doLogout(options); + break; + default: + Logger.reportError("SignInToWebsiteController", "Unknown observer notification:", aTopic); + break; + } + }, + + /* + * options: method required - name of method to invoke + * assertion optional + */ + _makeDoMethodCallback: function SignInToWebsiteController__makeDoMethodCallback(aRpId) { + return function SignInToWebsiteController_methodCallback(aOptions) { + let message = aOptions.json; + if (typeof message === 'string') { + message = JSON.parse(message); + } + + switch (message.method) { + case "ready": + IdentityService.doReady(aRpId); + break; + + case "login": + if (message._internalParams) { + IdentityService.doLogin(aRpId, message.assertion, message._internalParams); + } else { + IdentityService.doLogin(aRpId, message.assertion); + } + break; + + case "logout": + IdentityService.doLogout(aRpId); + break; + + case "cancel": + IdentityService.doCancel(aRpId); + break; + + default: + log("WARNING: wonky method call:", message.method); + break; + } + }; + }, + + doWatch: function SignInToWebsiteController_doWatch(aRpOptions) { + // dom prevents watch from being called twice + let contentOptions = { + message: kIdentityDelegateWatch, + showUI: false + }; + this.pipe.communicate(aRpOptions, contentOptions, + this._makeDoMethodCallback(aRpOptions.id)); + }, + + /** + * The website is requesting login so the user must choose an identity to use. + */ + doRequest: function SignInToWebsiteController_doRequest(aRpOptions) { + log("doRequest", aRpOptions); + let contentOptions = { + message: kIdentityDelegateRequest, + showUI: true + }; + this.pipe.communicate(aRpOptions, contentOptions, + this._makeDoMethodCallback(aRpOptions.id)); + }, + + /* + * + */ + doLogout: function SignInToWebsiteController_doLogout(aRpOptions) { + log("doLogout", aRpOptions); + let contentOptions = { + message: kIdentityDelegateLogout, + showUI: false + }; + this.pipe.communicate(aRpOptions, contentOptions, + this._makeDoMethodCallback(aRpOptions.id)); + } + +}; diff --git a/b2g/components/SimulatorScreen.js b/b2g/components/SimulatorScreen.js new file mode 100644 index 000000000..18d8f5cc4 --- /dev/null +++ b/b2g/components/SimulatorScreen.js @@ -0,0 +1,117 @@ +/* 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/. */ + +var Ci = Components.interfaces; +var Cu = Components.utils; + +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +Cu.import('resource://gre/modules/Services.jsm'); +Cu.import('resource://gre/modules/DOMRequestHelper.jsm'); + +XPCOMUtils.defineLazyModuleGetter(this, 'GlobalSimulatorScreen', + 'resource://gre/modules/GlobalSimulatorScreen.jsm'); + +var DEBUG_PREFIX = 'SimulatorScreen.js - '; +function debug() { + //dump(DEBUG_PREFIX + Array.slice(arguments) + '\n'); +} + +function fireOrientationEvent(window) { + let e = new window.Event('mozorientationchange'); + window.screen.dispatchEvent(e); +} + +function hookScreen(window) { + let nodePrincipal = window.document.nodePrincipal; + let origin = nodePrincipal.origin; + if (nodePrincipal.appStatus == nodePrincipal.APP_STATUS_NOT_INSTALLED) { + // Only inject screen mock for apps + return; + } + + let screen = window.wrappedJSObject.screen; + + screen.mozLockOrientation = function (orientation) { + debug('mozLockOrientation:', orientation, 'from', origin); + + // Normalize and do some checks against orientation input + if (typeof(orientation) == 'string') { + orientation = [orientation]; + } + + function isInvalidOrientationString(str) { + return typeof(str) != 'string' || + !str.match(/^default$|^(portrait|landscape)(-(primary|secondary))?$/); + } + if (!Array.isArray(orientation) || + orientation.some(isInvalidOrientationString)) { + Cu.reportError('Invalid orientation "' + orientation + '"'); + return false; + } + + GlobalSimulatorScreen.lock(orientation); + + return true; + }; + + screen.mozUnlockOrientation = function() { + debug('mozOrientationUnlock from', origin); + GlobalSimulatorScreen.unlock(); + return true; + }; + + Object.defineProperty(screen, 'width', { + get: () => GlobalSimulatorScreen.width + }); + Object.defineProperty(screen, 'height', { + get: () => GlobalSimulatorScreen.height + }); + Object.defineProperty(screen, 'mozOrientation', { + get: () => GlobalSimulatorScreen.mozOrientation + }); +} + +function SimulatorScreen() {} +SimulatorScreen.prototype = { + classID: Components.ID('{c83c02c0-5d43-4e3e-987f-9173b313e880}'), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + _windows: new Map(), + + observe: function (subject, topic, data) { + let windows = this._windows; + switch (topic) { + case 'profile-after-change': + Services.obs.addObserver(this, 'document-element-inserted', false); + Services.obs.addObserver(this, 'simulator-orientation-change', false); + Services.obs.addObserver(this, 'inner-window-destroyed', false); + break; + + case 'document-element-inserted': + let window = subject.defaultView; + if (!window) { + return; + } + + hookScreen(window); + + var id = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .currentInnerWindowID; + windows.set(id, window); + break; + + case 'inner-window-destroyed': + var id = subject.QueryInterface(Ci.nsISupportsPRUint64).data; + windows.delete(id); + break; + + case 'simulator-orientation-change': + windows.forEach(fireOrientationEvent); + break; + } + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SimulatorScreen]); diff --git a/b2g/components/SmsProtocolHandler.js b/b2g/components/SmsProtocolHandler.js new file mode 100644 index 000000000..c54018fcf --- /dev/null +++ b/b2g/components/SmsProtocolHandler.js @@ -0,0 +1,74 @@ +/* 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/. */ + +/** + * SmsProtocolHandle.js + * + * This file implements the URLs for SMS + * https://www.rfc-editor.org/rfc/rfc5724.txt + */ + +"use strict"; + + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +Cu.import("resource:///modules/TelURIParser.jsm"); +Cu.import('resource://gre/modules/ActivityChannel.jsm'); + +function SmsProtocolHandler() { +} + +SmsProtocolHandler.prototype = { + + scheme: "sms", + defaultPort: -1, + protocolFlags: Ci.nsIProtocolHandler.URI_NORELATIVE | + Ci.nsIProtocolHandler.URI_NOAUTH | + Ci.nsIProtocolHandler.URI_LOADABLE_BY_ANYONE | + Ci.nsIProtocolHandler.URI_DOES_NOT_RETURN_DATA, + allowPort: () => false, + + newURI: function Proto_newURI(aSpec, aOriginCharset) { + let uri = Cc["@mozilla.org/network/simple-uri;1"].createInstance(Ci.nsIURI); + uri.spec = aSpec; + return uri; + }, + + newChannel2: function Proto_newChannel2(aURI, aLoadInfo) { + let number = TelURIParser.parseURI('sms', aURI.spec); + let body = ""; + let query = aURI.spec.split("?")[1]; + + if (query) { + let params = query.split("&"); + params.forEach(function(aParam) { + let [name, value] = aParam.split("="); + if (name === "body") { + body = decodeURIComponent(value); + } + }) + } + + if (number || body) { + return new ActivityChannel(aURI, aLoadInfo, + "sms-handler", + { number: number || "", + type: "websms/sms", + body: body }); + } + + throw Components.results.NS_ERROR_ILLEGAL_VALUE; + }, + + newChannel: function Proto_newChannel(aURI) { + return this.newChannel2(aURI, null); + }, + + classID: Components.ID("{81ca20cb-0dad-4e32-8566-979c8998bd73}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolHandler]) +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SmsProtocolHandler]); diff --git a/b2g/components/SystemAppProxy.jsm b/b2g/components/SystemAppProxy.jsm new file mode 100644 index 000000000..b3d5843fc --- /dev/null +++ b/b2g/components/SystemAppProxy.jsm @@ -0,0 +1,377 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ +/* 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/XPCOMUtils.jsm'); +Cu.import('resource://gre/modules/Services.jsm'); + +this.EXPORTED_SYMBOLS = ['SystemAppProxy']; + +const kMainSystemAppId = 'main'; + +var SystemAppProxy = { + _frameInfoMap: new Map(), + _pendingLoadedEvents: [], + _pendingReadyEvents: [], + _pendingListeners: [], + + // To call when a main system app iframe is created + // Only used for main system app. + registerFrame: function systemApp_registerFrame(frame) { + this.registerFrameWithId(kMainSystemAppId, frame); + }, + + // To call when a new system(-remote) app iframe is created with ID + registerFrameWithId: function systemApp_registerFrameWithId(frameId, + frame) { + // - Frame ID of main system app is predefined as 'main'. + // - Frame ID of system-remote app is defined themselves. + // + // frameInfo = { + // isReady: ..., + // isLoaded: ..., + // frame: ... + // } + + let frameInfo = { frameId: frameId, + isReady: false, + isLoaded: false, + frame: frame }; + + this._frameInfoMap.set(frameId, frameInfo); + + // Register all DOM event listeners added before we got a ref to + // this system app iframe. + this._pendingListeners + .forEach(args => { + if (args[0] === frameInfo.frameId) { + this.addEventListenerWithId.apply(this, args); + } + }); + // Removed registered event listeners. + this._pendingListeners = + this._pendingListeners + .filter(args => { return args[0] != frameInfo.frameId; }); + }, + + unregisterFrameWithId: function systemApp_unregisterFrameWithId(frameId) { + this._frameInfoMap.delete(frameId); + // remove all pending event listener to the deleted system(-remote) app + this._pendingListeners = this._pendingListeners.filter( + args => { return args[0] != frameId; }); + this._pendingReadyEvents = this._pendingReadyEvents.filter( + ([evtFrameId]) => { return evtFrameId != frameId }); + this._pendingLoadedEvents = this._pendingLoadedEvents.filter( + ([evtFrameId]) => { return evtFrameId != frameId }); + }, + + // Get the main system app frame + _getMainSystemAppInfo: function systemApp_getMainSystemAppInfo() { + return this._frameInfoMap.get(kMainSystemAppId); + }, + + // Get the main system app frame + // Only used for the main system app. + getFrame: function systemApp_getFrame() { + return this.getFrameWithId(kMainSystemAppId); + }, + + // Get the frame of the specific system app + getFrameWithId: function systemApp_getFrameWithId(frameId) { + let frameInfo = this._frameInfoMap.get(frameId); + + if (!frameInfo) { + throw new Error('no frame ID is ' + frameId); + } + if (!frameInfo.frame) { + throw new Error('no content window'); + } + return frameInfo.frame; + }, + + // To call when the load event of the main system app document is triggered. + // i.e. everything that is not lazily loaded are run and done. + // Only used for the main system app. + setIsLoaded: function systemApp_setIsLoaded() { + this.setIsLoadedWithId(kMainSystemAppId); + }, + + // To call when the load event of the specific system app document is + // triggered. i.e. everything that is not lazily loaded are run and done. + setIsLoadedWithId: function systemApp_setIsLoadedWithId(frameId) { + let frameInfo = this._frameInfoMap.get(frameId); + if (!frameInfo) { + throw new Error('no frame ID is ' + frameId); + } + + if (frameInfo.isLoaded) { + if (frameInfo.frameId === kMainSystemAppId) { + Cu.reportError('SystemApp has already been declared as being loaded.'); + } + else { + Cu.reportError('SystemRemoteApp (ID: ' + frameInfo.frameId + ') ' + + 'has already been declared as being loaded.'); + } + } + + frameInfo.isLoaded = true; + + // Dispatch all events being queued while the system app was still loading + this._pendingLoadedEvents + .forEach(([evtFrameId, evtType, evtDetails]) => { + if (evtFrameId === frameInfo.frameId) { + this.sendCustomEventWithId(evtFrameId, evtType, evtDetails, true); + } + }); + // Remove sent events. + this._pendingLoadedEvents = + this._pendingLoadedEvents + .filter(([evtFrameId]) => { return evtFrameId != frameInfo.frameId }); + }, + + // To call when the main system app is ready to receive events + // i.e. when system-message-listener-ready mozContentEvent is sent. + // Only used for the main system app. + setIsReady: function systemApp_setIsReady() { + this.setIsReadyWithId(kMainSystemAppId); + }, + + // To call when the specific system(-remote) app is ready to receive events + // i.e. when system-message-listener-ready mozContentEvent is sent. + setIsReadyWithId: function systemApp_setIsReadyWithId(frameId) { + let frameInfo = this._frameInfoMap.get(frameId); + if (!frameInfo) { + throw new Error('no frame ID is ' + frameId); + } + + if (!frameInfo.isLoaded) { + Cu.reportError('SystemApp.setIsLoaded() should be called before setIsReady().'); + } + + if (frameInfo.isReady) { + Cu.reportError('SystemApp has already been declared as being ready.'); + } + + frameInfo.isReady = true; + + // Dispatch all events being queued while the system app was still not ready + this._pendingReadyEvents + .forEach(([evtFrameId, evtType, evtDetails]) => { + if (evtFrameId === frameInfo.frameId) { + this.sendCustomEventWithId(evtFrameId, evtType, evtDetails); + } + }); + + // Remove sent events. + this._pendingReadyEvents = + this._pendingReadyEvents + .filter(([evtFrameId]) => { return evtFrameId != frameInfo.frameId }); + }, + + /* + * Common way to send an event to the main system app. + * Only used for the main system app. + * + * // In gecko code: + * SystemAppProxy.sendCustomEvent('foo', { data: 'bar' }); + * // In system app: + * window.addEventListener('foo', function (event) { + * event.details == 'bar' + * }); + * + * @param type The custom event type. + * @param details The event details. + * @param noPending Set to true to emit this event even before the system + * app is ready. + * Event is always pending if the app is not loaded yet. + * @param target The element who dispatch this event. + * + * @returns event? Dispatched event, or null if the event is pending. + */ + _sendCustomEvent: function systemApp_sendCustomEvent(type, + details, + noPending, + target) { + let args = Array.prototype.slice.call(arguments); + return this.sendCustomEventWithId + .apply(this, [kMainSystemAppId].concat(args)); + }, + + /* + * Common way to send an event to the specific system app. + * + * // In gecko code (send custom event from main system app): + * SystemAppProxy.sendCustomEventWithId('main', 'foo', { data: 'bar' }); + * // In system app: + * window.addEventListener('foo', function (event) { + * event.details == 'bar' + * }); + * + * @param frameId Specify the system(-remote) app who dispatch this event. + * @param type The custom event type. + * @param details The event details. + * @param noPending Set to true to emit this event even before the system + * app is ready. + * Event is always pending if the app is not loaded yet. + * @param target The element who dispatch this event. + * + * @returns event? Dispatched event, or null if the event is pending. + */ + sendCustomEventWithId: function systemApp_sendCustomEventWithId(frameId, + type, + details, + noPending, + target) { + let frameInfo = this._frameInfoMap.get(frameId); + let content = (frameInfo && frameInfo.frame) ? + frameInfo.frame.contentWindow : null; + // If the system app isn't loaded yet, + // queue events until someone calls setIsLoaded + if (!content || !(frameInfo && frameInfo.isLoaded)) { + if (noPending) { + this._pendingLoadedEvents.push([frameId, type, details]); + } else { + this._pendingReadyEvents.push([frameId, type, details]); + } + return null; + } + + // If the system app isn't ready yet, + // queue events until someone calls setIsReady + if (!(frameInfo && frameInfo.isReady) && !noPending) { + this._pendingReadyEvents.push([frameId, type, details]); + return null; + } + + let event = content.document.createEvent('CustomEvent'); + + let payload; + // If the root object already has __exposedProps__, + // we consider the caller already wrapped (correctly) the object. + if ('__exposedProps__' in details) { + payload = details; + } else { + payload = details ? Cu.cloneInto(details, content) : {}; + } + + if ((target || content) === frameInfo.frame.contentWindow) { + dump('XXX FIXME : Dispatch a ' + type + ': ' + details.type + '\n'); + } + + event.initCustomEvent(type, true, false, payload); + (target || content).dispatchEvent(event); + + return event; + }, + + // Now deprecated, use sendCustomEvent with a custom event name + dispatchEvent: function systemApp_dispatchEvent(details, target) { + return this._sendCustomEvent('mozChromeEvent', details, false, target); + }, + + dispatchKeyboardEvent: function systemApp_dispatchKeyboardEvent(type, details) { + try { + let frameInfo = this._getMainSystemAppInfo(); + let content = (frameInfo && frameInfo.frame) ? frameInfo.frame.contentWindow + : null; + if (!content) { + throw new Error('no content window'); + } + // If we don't already have a TextInputProcessor, create one now + if (!this.TIP) { + this.TIP = Cc['@mozilla.org/text-input-processor;1'] + .createInstance(Ci.nsITextInputProcessor); + if (!this.TIP) { + throw new Error('failed to create textInputProcessor'); + } + } + + if (!this.TIP.beginInputTransactionForTests(content)) { + this.TIP = null; + throw new Error('beginInputTransaction failed'); + } + + let e = new content.KeyboardEvent('', { key: details.key, }); + + if (type === 'keydown') { + this.TIP.keydown(e); + } + else if (type === 'keyup') { + this.TIP.keyup(e); + } + else { + throw new Error('unexpected event type: ' + type); + } + } + catch(e) { + dump('dispatchKeyboardEvent: ' + e + '\n'); + } + }, + + // Listen for dom events on the main system app + addEventListener: function systemApp_addEventListener() { + let args = Array.prototype.slice.call(arguments); + this.addEventListenerWithId.apply(this, [kMainSystemAppId].concat(args)); + }, + + // Listen for dom events on the specific system app + addEventListenerWithId: function systemApp_addEventListenerWithId(frameId, + ...args) { + let frameInfo = this._frameInfoMap.get(frameId); + + if (!frameInfo) { + this._pendingListeners.push(arguments); + return false; + } + + let content = frameInfo.frame.contentWindow; + content.addEventListener.apply(content, args); + return true; + }, + + // remove the event listener from the main system app + removeEventListener: function systemApp_removeEventListener(name, listener) { + this.removeEventListenerWithId.apply(this, [kMainSystemAppId, name, listener]); + }, + + // remove the event listener from the specific system app + removeEventListenerWithId: function systemApp_removeEventListenerWithId(frameId, + name, + listener) { + let frameInfo = this._frameInfoMap.get(frameId); + + if (frameInfo) { + let content = frameInfo.frame.contentWindow; + content.removeEventListener.apply(content, [name, listener]); + } + else { + this._pendingListeners = this._pendingListeners.filter( + args => { + return args[0] != frameId || args[1] != name || args[2] != listener; + }); + } + }, + + // Get all frame in system app + getFrames: function systemApp_getFrames(frameId) { + let frameList = []; + + for (let frameId of this._frameInfoMap.keys()) { + let frameInfo = this._frameInfoMap.get(frameId); + let systemAppFrame = frameInfo.frame; + let subFrames = systemAppFrame.contentDocument.querySelectorAll('iframe'); + frameList.push(systemAppFrame); + for (let i = 0; i < subFrames.length; ++i) { + frameList.push(subFrames[i]); + } + } + return frameList; + } +}; +this.SystemAppProxy = SystemAppProxy; diff --git a/b2g/components/SystemMessageInternal.js b/b2g/components/SystemMessageInternal.js new file mode 100644 index 000000000..287496a50 --- /dev/null +++ b/b2g/components/SystemMessageInternal.js @@ -0,0 +1,64 @@ +/* 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 Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/SystemAppProxy.jsm"); + +function debug(aMsg) { + dump("-- SystemMessageInternal " + Date.now() + " : " + aMsg + "\n"); +} + +// Implementation of the component used by internal users. + +function SystemMessageInternal() { +} + +SystemMessageInternal.prototype = { + + sendMessage: function(aType, aMessage, aPageURI, aManifestURI, aExtra) { + debug(`sendMessage ${aType} ${aMessage} ${aPageURI} ${aExtra}`); + SystemAppProxy._sendCustomEvent("mozSystemMessage", { + action: "send", + type: aType, + message: aMessage, + pageURI: aPageURI, + extra: aExtra + }); + return Promise.resolve(); + }, + + broadcastMessage: function(aType, aMessage, aExtra) { + debug(`broadcastMessage ${aType} ${aMessage} ${aExtra}`); + SystemAppProxy._sendCustomEvent("mozSystemMessage", { + action: "broadcast", + type: aType, + message: aMessage, + extra: aExtra + }); + return Promise.resolve(); + }, + + registerPage: function(aType, aPageURI, aManifestURI) { + SystemAppProxy._sendCustomEvent("mozSystemMessage", { + action: "register", + type: aType, + pageURI: aPageURI + }); + debug(`registerPage ${aType} ${aPageURI} ${aManifestURI}`); + }, + + classID: Components.ID("{70589ca5-91ac-4b9e-b839-d6a88167d714}"), + + QueryInterface: XPCOMUtils.generateQI([Ci.nsISystemMessagesInternal]) +} + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SystemMessageInternal]); diff --git a/b2g/components/TelProtocolHandler.js b/b2g/components/TelProtocolHandler.js new file mode 100644 index 000000000..021cbcd61 --- /dev/null +++ b/b2g/components/TelProtocolHandler.js @@ -0,0 +1,60 @@ +/* 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/. */ + +/** + * TelProtocolHandle.js + * + * This file implements the URLs for Telephone Calls + * https://www.ietf.org/rfc/rfc2806.txt + */ + +"use strict"; + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +Cu.import("resource:///modules/TelURIParser.jsm"); +Cu.import('resource://gre/modules/ActivityChannel.jsm'); + +function TelProtocolHandler() { +} + +TelProtocolHandler.prototype = { + + scheme: "tel", + defaultPort: -1, + protocolFlags: Ci.nsIProtocolHandler.URI_NORELATIVE | + Ci.nsIProtocolHandler.URI_NOAUTH | + Ci.nsIProtocolHandler.URI_LOADABLE_BY_ANYONE | + Ci.nsIProtocolHandler.URI_DOES_NOT_RETURN_DATA, + allowPort: () => false, + + newURI: function Proto_newURI(aSpec, aOriginCharset) { + let uri = Cc["@mozilla.org/network/simple-uri;1"].createInstance(Ci.nsIURI); + uri.spec = aSpec; + return uri; + }, + + newChannel2: function Proto_newChannel(aURI, aLoadInfo) { + let number = TelURIParser.parseURI('tel', aURI.spec); + + if (number) { + return new ActivityChannel(aURI, aLoadInfo, + "dial-handler", + { number: number, + type: "webtelephony/number" }); + } + + throw Components.results.NS_ERROR_ILLEGAL_VALUE; + }, + + newChannel: function Proto_newChannel(aURI) { + return this.newChannel2(aURI, null); + }, + + classID: Components.ID("{782775dd-7351-45ea-aff1-0ffa872cfdd2}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolHandler]) +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([TelProtocolHandler]); diff --git a/b2g/components/TelURIParser.jsm b/b2g/components/TelURIParser.jsm new file mode 100644 index 000000000..46b0bb8fd --- /dev/null +++ b/b2g/components/TelURIParser.jsm @@ -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 = ["TelURIParser"]; + +/** + * Singleton providing functionality for parsing tel: and sms: URIs + */ +this.TelURIParser = { + parseURI: function(scheme, uri) { + // https://www.ietf.org/rfc/rfc2806.txt + let subscriber = decodeURIComponent(uri.slice((scheme + ':').length)); + + if (!subscriber.length) { + return null; + } + + let number = ''; + let pos = 0; + let len = subscriber.length; + + // visual-separator + let visualSeparator = [ ' ', '-', '.', '(', ')' ]; + let digits = [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' ]; + let dtmfDigits = [ '*', '#', 'A', 'B', 'C', 'D' ]; + let pauseCharacter = [ 'p', 'w' ]; + + // global-phone-number + if (subscriber[pos] == '+') { + number += '+'; + for (++pos; pos < len; ++pos) { + if (visualSeparator.indexOf(subscriber[pos]) != -1) { + number += subscriber[pos]; + } else if (digits.indexOf(subscriber[pos]) != -1) { + number += subscriber[pos]; + } else { + break; + } + } + } + // local-phone-number + else { + for (; pos < len; ++pos) { + if (visualSeparator.indexOf(subscriber[pos]) != -1) { + number += subscriber[pos]; + } else if (digits.indexOf(subscriber[pos]) != -1) { + number += subscriber[pos]; + } else if (dtmfDigits.indexOf(subscriber[pos]) != -1) { + number += subscriber[pos]; + } else if (pauseCharacter.indexOf(subscriber[pos]) != -1) { + number += subscriber[pos]; + } else { + break; + } + } + + // this means error + if (!number.length) { + return null; + } + + // isdn-subaddress + if (subscriber.substring(pos, pos + 6) == ';isub=') { + let subaddress = ''; + + for (pos += 6; pos < len; ++pos) { + if (visualSeparator.indexOf(subscriber[pos]) != -1) { + subaddress += subscriber[pos]; + } else if (digits.indexOf(subscriber[pos]) != -1) { + subaddress += subscriber[pos]; + } else { + break; + } + } + + // FIXME: ignore subaddress - Bug 795242 + } + + // post-dial + if (subscriber.substring(pos, pos + 7) == ';postd=') { + let subaddress = ''; + + for (pos += 7; pos < len; ++pos) { + if (visualSeparator.indexOf(subscriber[pos]) != -1) { + subaddress += subscriber[pos]; + } else if (digits.indexOf(subscriber[pos]) != -1) { + subaddress += subscriber[pos]; + } else if (dtmfDigits.indexOf(subscriber[pos]) != -1) { + subaddress += subscriber[pos]; + } else if (pauseCharacter.indexOf(subscriber[pos]) != -1) { + subaddress += subscriber[pos]; + } else { + break; + } + } + + // FIXME: ignore subaddress - Bug 795242 + } + + // area-specific + if (subscriber.substring(pos, pos + 15) == ';phone-context=') { + pos += 15; + + // global-network-prefix | local-network-prefix | private-prefi + number = subscriber.substring(pos, subscriber.length) + number; + } + } + + // Ignore MWI and USSD codes. See 794034. + if (number.match(/[#\*]/) && !number.match(/^[#\*]\d+$/)) { + return null; + } + + return number || null; + } +}; + diff --git a/b2g/components/UpdatePrompt.js b/b2g/components/UpdatePrompt.js new file mode 100644 index 000000000..1df07204c --- /dev/null +++ b/b2g/components/UpdatePrompt.js @@ -0,0 +1,783 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=8 et : + */ +/* 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 Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/AppConstants.jsm"); + +const VERBOSE = 1; +var log = + VERBOSE ? + function log_dump(msg) { dump("UpdatePrompt: "+ msg +"\n"); } : + function log_noop(msg) { }; + +const PREF_APPLY_PROMPT_TIMEOUT = "b2g.update.apply-prompt-timeout"; +const PREF_APPLY_IDLE_TIMEOUT = "b2g.update.apply-idle-timeout"; +const PREF_DOWNLOAD_WATCHDOG_TIMEOUT = "b2g.update.download-watchdog-timeout"; +const PREF_DOWNLOAD_WATCHDOG_MAX_RETRIES = "b2g.update.download-watchdog-max-retries"; + +const NETWORK_ERROR_OFFLINE = 111; +const HTTP_ERROR_OFFSET = 1000; + +const STATE_DOWNLOADING = 'downloading'; + +XPCOMUtils.defineLazyServiceGetter(Services, "aus", + "@mozilla.org/updates/update-service;1", + "nsIApplicationUpdateService"); + +XPCOMUtils.defineLazyServiceGetter(Services, "um", + "@mozilla.org/updates/update-manager;1", + "nsIUpdateManager"); + +XPCOMUtils.defineLazyServiceGetter(Services, "idle", + "@mozilla.org/widget/idleservice;1", + "nsIIdleService"); + +XPCOMUtils.defineLazyServiceGetter(Services, "settings", + "@mozilla.org/settingsService;1", + "nsISettingsService"); + +XPCOMUtils.defineLazyServiceGetter(Services, 'env', + '@mozilla.org/process/environment;1', + 'nsIEnvironment'); + +function useSettings() { + // When we're running in the real phone, then we can use settings. + // But when we're running as part of xpcshell, there is no settings database + // and trying to use settings in this scenario causes lots of weird + // assertions at shutdown time. + if (typeof useSettings.result === "undefined") { + useSettings.result = !Services.env.get("XPCSHELL_TEST_PROFILE_DIR"); + } + return useSettings.result; +} + +XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy", + "resource://gre/modules/SystemAppProxy.jsm"); + +function UpdateCheckListener(updatePrompt) { + this._updatePrompt = updatePrompt; +} + +UpdateCheckListener.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateCheckListener]), + + _updatePrompt: null, + + onCheckComplete: function UCL_onCheckComplete(request, updates, updateCount) { + if (Services.um.activeUpdate) { + // We're actively downloading an update, that's the update the user should + // see, even if a newer update is available. + this._updatePrompt.setUpdateStatus("active-update"); + this._updatePrompt.showUpdateAvailable(Services.um.activeUpdate); + return; + } + + if (updateCount == 0) { + this._updatePrompt.setUpdateStatus("no-updates"); + + if (this._updatePrompt._systemUpdateListener) { + this._updatePrompt._systemUpdateListener.onError("no-updates"); + } + + return; + } + + let update = Services.aus.selectUpdate(updates, updateCount); + if (!update) { + this._updatePrompt.setUpdateStatus("already-latest-version"); + + if (this._updatePrompt._systemUpdateListener) { + this._updatePrompt._systemUpdateListener.onError("already-latest-version"); + } + + return; + } + + this._updatePrompt.setUpdateStatus("check-complete"); + this._updatePrompt.showUpdateAvailable(update); + }, + + onError: function UCL_onError(request, update) { + // nsIUpdate uses a signed integer for errorCode while any platform errors + // require all 32 bits. + let errorCode = update.errorCode >>> 0; + let isNSError = (errorCode >>> 31) == 1; + let errorMsg = "check-error-"; + + if (errorCode == NETWORK_ERROR_OFFLINE) { + errorMsg = "retry-when-online"; + this._updatePrompt.setUpdateStatus(errorMsg); + } else if (isNSError) { + errorMsg = "check-error-" + errorCode; + this._updatePrompt.setUpdateStatus(errorMsg); + } else if (errorCode > HTTP_ERROR_OFFSET) { + let httpErrorCode = errorCode - HTTP_ERROR_OFFSET; + errorMsg = "check-error-http-" + httpErrorCode; + this._updatePrompt.setUpdateStatus(errorMsg); + } + + if (this._updatePrompt._systemUpdateListener) { + this._updatePrompt._systemUpdateListener.onError(errorMsg); + } + + Services.aus.QueryInterface(Ci.nsIUpdateCheckListener); + Services.aus.onError(request, update); + } +}; + +function UpdatePrompt() { + this.wrappedJSObject = this; + this._updateCheckListener = new UpdateCheckListener(this); +} + +UpdatePrompt.prototype = { + classID: Components.ID("{88b3eb21-d072-4e3b-886d-f89d8c49fe59}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdatePrompt, + Ci.nsIUpdateCheckListener, + Ci.nsIRequestObserver, + Ci.nsIProgressEventSink, + Ci.nsIObserver, + Ci.nsISystemUpdateProvider]), + _xpcom_factory: XPCOMUtils.generateSingletonFactory(UpdatePrompt), + + _update: null, + _applyPromptTimer: null, + _waitingForIdle: false, + _updateCheckListner: null, + _systemUpdateListener: null, + _availableParameters: { + "deviceinfo.last_updated": null, + "gecko.updateStatus": null, + "app.update.channel": null, + "app.update.interval": null, + "app.update.url": null, + }, + _pendingUpdateAvailablePackageInfo: null, + _isPendingUpdateReady: false, + _updateErrorQueue: [ ], + _receivedUpdatePromptReady: false, + + // nsISystemUpdateProvider + checkForUpdate: function() { + this.forceUpdateCheck(); + }, + + startDownload: function() { + this.downloadUpdate(this._update); + }, + + stopDownload: function() { + this.handleDownloadCancel(); + }, + + applyUpdate: function() { + this.handleApplyPromptResult({result: "restart"}); + }, + + setParameter: function(aName, aValue) { + if (!this._availableParameters.hasOwnProperty(aName)) { + return false; + } + + this._availableParameters[aName] = aValue; + + switch (aName) { + case "app.update.channel": + case "app.update.url": + Services.prefs.setCharPref(aName, aValue); + break; + case "app.update.interval": + Services.prefs.setIntPref(aName, parseInt(aValue, 10)); + break; + } + + return true; + }, + + getParameter: function(aName) { + if (!this._availableParameters.hasOwnProperty(aName)) { + return null; + } + + return this._availableParameters[aName]; + }, + + setListener: function(aListener) { + this._systemUpdateListener = aListener; + + // If an update is available or ready, trigger the event right away at this point. + if (this._pendingUpdateAvailablePackageInfo) { + this._systemUpdateListener.onUpdateAvailable(this._pendingUpdateAvailablePackageInfo.type, + this._pendingUpdateAvailablePackageInfo.version, + this._pendingUpdateAvailablePackageInfo.description, + this._pendingUpdateAvailablePackageInfo.buildDate, + this._pendingUpdateAvailablePackageInfo.size); + // Set null when the listener is attached. + this._pendingUpdateAvailablePackageInfo = null; + } + + if (this._isPendingUpdateReady) { + this._systemUpdateListener.onUpdateReady(); + this._isPendingUpdateReady = false; + } + }, + + unsetListener: function(aListener) { + this._systemUpdateListener = null; + }, + + get applyPromptTimeout() { + return Services.prefs.getIntPref(PREF_APPLY_PROMPT_TIMEOUT); + }, + + get applyIdleTimeout() { + return Services.prefs.getIntPref(PREF_APPLY_IDLE_TIMEOUT); + }, + + handleContentStart: function UP_handleContentStart() { + SystemAppProxy.addEventListener("mozContentEvent", this); + }, + + // nsIUpdatePrompt + + // FIXME/bug 737601: we should have users opt-in to downloading + // updates when on a billed pipe. Initially, opt-in for 3g, but + // that doesn't cover all cases. + checkForUpdates: function UP_checkForUpdates() { }, + + showUpdateAvailable: function UP_showUpdateAvailable(aUpdate) { + let packageInfo = {}; + packageInfo.version = aUpdate.displayVersion; + packageInfo.description = aUpdate.statusText; + packageInfo.buildDate = aUpdate.buildID; + + let patch = aUpdate.selectedPatch; + if (!patch && aUpdate.patchCount > 0) { + // For now we just check the first patch to get size information if a + // patch hasn't been selected yet. + patch = aUpdate.getPatchAt(0); + } + + if (patch) { + packageInfo.size = patch.size; + packageInfo.type = patch.type; + } else { + log("Warning: no patches available in update"); + } + + this._pendingUpdateAvailablePackageInfo = packageInfo; + + if (this._systemUpdateListener) { + this._systemUpdateListener.onUpdateAvailable(packageInfo.type, + packageInfo.version, + packageInfo.description, + packageInfo.buildDate, + packageInfo.size); + // Set null since the event is fired. + this._pendingUpdateAvailablePackageInfo = null; + } + + if (!this.sendUpdateEvent("update-available", aUpdate)) { + + log("Unable to prompt for available update, forcing download"); + this.downloadUpdate(aUpdate); + } + }, + + showUpdateDownloaded: function UP_showUpdateDownloaded(aUpdate, aBackground) { + if (this._systemUpdateListener) { + this._systemUpdateListener.onUpdateReady(); + } else { + this._isPendingUpdateReady = true; + } + + // The update has been downloaded and staged. We send the update-downloaded + // event right away. After the user has been idle for a while, we send the + // update-prompt-restart event, increasing the chances that we can apply the + // update quietly without user intervention. + this.sendUpdateEvent("update-downloaded", aUpdate); + + if (Services.idle.idleTime >= this.applyIdleTimeout) { + this.showApplyPrompt(aUpdate); + return; + } + + let applyIdleTimeoutSeconds = this.applyIdleTimeout / 1000; + // We haven't been idle long enough, so register an observer + log("Update is ready to apply, registering idle timeout of " + + applyIdleTimeoutSeconds + " seconds before prompting."); + + this._update = aUpdate; + this.waitForIdle(); + }, + + storeUpdateError: function UP_storeUpdateError(aUpdate) { + log("Storing update error for later use"); + this._updateErrorQueue.push(aUpdate); + }, + + sendStoredUpdateError: function UP_sendStoredUpdateError() { + log("Sending stored update error"); + this._updateErrorQueue.forEach(aUpdate => { + this.sendUpdateEvent("update-error", aUpdate); + }); + this._updateErrorQueue = [ ]; + }, + + showUpdateError: function UP_showUpdateError(aUpdate) { + log("Update error, state: " + aUpdate.state + ", errorCode: " + + aUpdate.errorCode); + if (this._systemUpdateListener) { + this._systemUpdateListener.onError("update-error: " + aUpdate.errorCode + " " + aUpdate.statusText); + } + + if (!this._receivedUpdatePromptReady) { + this.storeUpdateError(aUpdate); + } else { + this.sendUpdateEvent("update-error", aUpdate); + } + + this.setUpdateStatus(aUpdate.statusText); + }, + + showUpdateHistory: function UP_showUpdateHistory(aParent) { }, + showUpdateInstalled: function UP_showUpdateInstalled() { + this.setParameter("deviceinfo.last_updated", Date.now()); + + if (useSettings()) { + let lock = Services.settings.createLock(); + lock.set("deviceinfo.last_updated", Date.now(), null, null); + } + }, + + // Custom functions + + waitForIdle: function UP_waitForIdle() { + if (this._waitingForIdle) { + return; + } + + this._waitingForIdle = true; + Services.idle.addIdleObserver(this, this.applyIdleTimeout / 1000); + Services.obs.addObserver(this, "quit-application", false); + }, + + setUpdateStatus: function UP_setUpdateStatus(aStatus) { + this.setParameter("gecko.updateStatus", aStatus); + + if (useSettings()) { + log("Setting gecko.updateStatus: " + aStatus); + + let lock = Services.settings.createLock(); + lock.set("gecko.updateStatus", aStatus, null); + } + }, + + showApplyPrompt: function UP_showApplyPrompt(aUpdate) { + // Notify update package is ready to apply + if (this._systemUpdateListener) { + this._systemUpdateListener.onUpdateReady(); + } else { + // Set the flag to true and fire the onUpdateReady event when the listener is attached. + this._isPendingUpdateReady = true; + } + + if (!this.sendUpdateEvent("update-prompt-apply", aUpdate)) { + log("Unable to prompt, forcing restart"); + this.restartProcess(); + return; + } + + if (AppConstants.MOZ_B2G_RIL) { + let window = Services.wm.getMostRecentWindow("navigator:browser"); + let pinReq = window.navigator.mozIccManager.getCardLock("pin"); + pinReq.onsuccess = function(e) { + if (e.target.result.enabled) { + // The SIM is pin locked. Don't use a fallback timer. This means that + // the user has to press Install to apply the update. If we use the + // timer, and the timer reboots the phone, then the phone will be + // unusable until the SIM is unlocked. + log("SIM is pin locked. Not starting fallback timer."); + } else { + // This means that no pin lock is enabled, so we go ahead and start + // the fallback timer. + this._applyPromptTimer = this.createTimer(this.applyPromptTimeout); + } + }.bind(this); + pinReq.onerror = function(e) { + this._applyPromptTimer = this.createTimer(this.applyPromptTimeout); + }.bind(this); + } else { + // Schedule a fallback timeout in case the UI is unable to respond or show + // a prompt for some reason. + this._applyPromptTimer = this.createTimer(this.applyPromptTimeout); + } + }, + + _copyProperties: ["appVersion", "buildID", "detailsURL", "displayVersion", + "errorCode", "isOSUpdate", "platformVersion", + "previousAppVersion", "state", "statusText"], + + sendUpdateEvent: function UP_sendUpdateEvent(aType, aUpdate) { + let detail = {}; + for (let property of this._copyProperties) { + detail[property] = aUpdate[property]; + } + + let patch = aUpdate.selectedPatch; + if (!patch && aUpdate.patchCount > 0) { + // For now we just check the first patch to get size information if a + // patch hasn't been selected yet. + patch = aUpdate.getPatchAt(0); + } + + if (patch) { + detail.size = patch.size; + detail.updateType = patch.type; + } else { + log("Warning: no patches available in update"); + } + + this._update = aUpdate; + return this.sendChromeEvent(aType, detail); + }, + + sendChromeEvent: function UP_sendChromeEvent(aType, aDetail) { + let detail = aDetail || {}; + detail.type = aType; + + let sent = SystemAppProxy.dispatchEvent(detail); + if (!sent) { + log("Warning: Couldn't send update event " + aType + + ": no content browser. Will send again when content becomes available."); + return false; + } + return true; + }, + + handleAvailableResult: function UP_handleAvailableResult(aDetail) { + // If the user doesn't choose "download", the updater will implicitly call + // showUpdateAvailable again after a certain period of time + switch (aDetail.result) { + case "download": + this.downloadUpdate(this._update); + break; + } + }, + + handleApplyPromptResult: function UP_handleApplyPromptResult(aDetail) { + if (this._applyPromptTimer) { + this._applyPromptTimer.cancel(); + this._applyPromptTimer = null; + } + + switch (aDetail.result) { + // Battery not okay, do not wait for idle to re-prompt + case "low-battery": + break; + case "wait": + // Wait until the user is idle before prompting to apply the update + this.waitForIdle(); + break; + case "restart": + this.finishUpdate(); + this._update = null; + break; + } + }, + + downloadUpdate: function UP_downloadUpdate(aUpdate) { + if (!aUpdate) { + aUpdate = Services.um.activeUpdate; + if (!aUpdate) { + log("No active update found to download"); + return; + } + } + + let status = Services.aus.downloadUpdate(aUpdate, true); + if (status == STATE_DOWNLOADING) { + Services.aus.addDownloadListener(this); + return; + } + + // If the update has already been downloaded and applied, then + // Services.aus.downloadUpdate will return immediately and not + // call showUpdateDownloaded, so we detect this. + if (aUpdate.state == "applied" && aUpdate.errorCode == 0) { + this.showUpdateDownloaded(aUpdate, true); + return; + } + + log("Error downloading update " + aUpdate.name + ": " + aUpdate.errorCode); + let errorCode = aUpdate.errorCode >>> 0; + if (errorCode == Cr.NS_ERROR_FILE_TOO_BIG) { + aUpdate.statusText = "file-too-big"; + } + this.showUpdateError(aUpdate); + }, + + handleDownloadCancel: function UP_handleDownloadCancel() { + log("Pausing download"); + Services.aus.pauseDownload(); + }, + + finishUpdate: function UP_finishUpdate() { + if (!this._update.isOSUpdate) { + // Standard gecko+gaia updates will just need to restart the process + this.restartProcess(); + return; + } + + try { + Services.aus.applyOsUpdate(this._update); + } + catch (e) { + this._update.errorCode = Cr.NS_ERROR_FAILURE; + this.showUpdateError(this._update); + } + }, + + restartProcess: function UP_restartProcess() { + log("Update downloaded, restarting to apply it"); + + let callbackAfterSet = function() { + if (AppConstants.platform !== "gonk") { + let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"] + .getService(Ci.nsIAppStartup); + appStartup.quit(appStartup.eForceQuit | appStartup.eRestart); + } else { + // NB: on Gonk, we rely on the system process manager to restart us. + let pmService = Cc["@mozilla.org/power/powermanagerservice;1"] + .getService(Ci.nsIPowerManagerService); + pmService.restart(); + } + } + + if (useSettings()) { + // Save current os version in deviceinfo.previous_os + let lock = Services.settings.createLock({ + handle: callbackAfterSet, + handleAbort: function(error) { + log("Abort callback when trying to set previous_os: " + error); + callbackAfterSet(); + } + }); + lock.get("deviceinfo.os", { + handle: function(name, value) { + log("Set previous_os to: " + value); + lock.set("deviceinfo.previous_os", value, null, null); + } + }); + } + }, + + forceUpdateCheck: function UP_forceUpdateCheck() { + log("Forcing update check"); + + let checker = Cc["@mozilla.org/updates/update-checker;1"] + .createInstance(Ci.nsIUpdateChecker); + checker.checkForUpdates(this._updateCheckListener, true); + }, + + handleEvent: function UP_handleEvent(evt) { + if (evt.type !== "mozContentEvent") { + return; + } + + let detail = evt.detail; + if (!detail) { + return; + } + + switch (detail.type) { + case "force-update-check": + this.forceUpdateCheck(); + break; + case "update-available-result": + this.handleAvailableResult(detail); + // If we started the apply prompt timer, this means that we're waiting + // for the user to press Later or Install Now. In this situation we + // don't want to clear this._update, becuase handleApplyPromptResult + // needs it. + if (this._applyPromptTimer == null && !this._waitingForIdle) { + this._update = null; + } + break; + case "update-download-cancel": + this.handleDownloadCancel(); + break; + case "update-prompt-apply-result": + this.handleApplyPromptResult(detail); + break; + case "update-prompt-ready": + this._receivedUpdatePromptReady = true; + this.sendStoredUpdateError(); + break; + } + }, + + // nsIObserver + + observe: function UP_observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "idle": + this._waitingForIdle = false; + this.showApplyPrompt(this._update); + // Fall through + case "quit-application": + Services.idle.removeIdleObserver(this, this.applyIdleTimeout / 1000); + Services.obs.removeObserver(this, "quit-application"); + break; + } + }, + + // nsITimerCallback + + notify: function UP_notify(aTimer) { + if (aTimer == this._applyPromptTimer) { + log("Timed out waiting for result, restarting"); + this._applyPromptTimer = null; + this.finishUpdate(); + this._update = null; + return; + } + if (aTimer == this._watchdogTimer) { + log("Download watchdog fired"); + this._watchdogTimer = null; + this._autoRestartDownload = true; + Services.aus.pauseDownload(); + return; + } + }, + + createTimer: function UP_createTimer(aTimeoutMs) { + let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback(this, aTimeoutMs, timer.TYPE_ONE_SHOT); + return timer; + }, + + // nsIRequestObserver + + _startedSent: false, + + _watchdogTimer: null, + + _autoRestartDownload: false, + _autoRestartCount: 0, + + startWatchdogTimer: function UP_startWatchdogTimer() { + let watchdogTimeout = 120000; // 120 seconds + try { + watchdogTimeout = Services.prefs.getIntPref(PREF_DOWNLOAD_WATCHDOG_TIMEOUT); + } catch (e) { + // This means that the preference doesn't exist. watchdogTimeout will + // retain its default assigned above. + } + if (watchdogTimeout <= 0) { + // 0 implies don't bother using the watchdog timer at all. + this._watchdogTimer = null; + return; + } + if (this._watchdogTimer) { + this._watchdogTimer.cancel(); + } else { + this._watchdogTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + } + this._watchdogTimer.initWithCallback(this, watchdogTimeout, + Ci.nsITimer.TYPE_ONE_SHOT); + }, + + stopWatchdogTimer: function UP_stopWatchdogTimer() { + if (this._watchdogTimer) { + this._watchdogTimer.cancel(); + this._watchdogTimer = null; + } + }, + + touchWatchdogTimer: function UP_touchWatchdogTimer() { + this.startWatchdogTimer(); + }, + + onStartRequest: function UP_onStartRequest(aRequest, aContext) { + // Wait until onProgress to send the update-download-started event, in case + // this request turns out to fail for some reason + this._startedSent = false; + this.startWatchdogTimer(); + }, + + onStopRequest: function UP_onStopRequest(aRequest, aContext, aStatusCode) { + this.stopWatchdogTimer(); + Services.aus.removeDownloadListener(this); + let paused = !Components.isSuccessCode(aStatusCode); + if (!paused) { + // The download was successful, no need to restart + this._autoRestartDownload = false; + } + if (this._autoRestartDownload) { + this._autoRestartDownload = false; + let watchdogMaxRetries = Services.prefs.getIntPref(PREF_DOWNLOAD_WATCHDOG_MAX_RETRIES); + this._autoRestartCount++; + if (this._autoRestartCount > watchdogMaxRetries) { + log("Download - retry count exceeded - error"); + // We exceeded the max retries. Treat the download like an error, + // which will give the user a chance to restart manually later. + this._autoRestartCount = 0; + if (Services.um.activeUpdate) { + this.showUpdateError(Services.um.activeUpdate); + } + return; + } + log("Download - restarting download - attempt " + this._autoRestartCount); + this.downloadUpdate(null); + return; + } + this._autoRestartCount = 0; + this.sendChromeEvent("update-download-stopped", { + paused: paused + }); + }, + + // nsIProgressEventSink + + onProgress: function UP_onProgress(aRequest, aContext, aProgress, + aProgressMax) { + if (this._systemUpdateListener) { + this._systemUpdateListener.onProgress(aProgress, aProgressMax); + } + + if (aProgress == aProgressMax) { + // The update.mar validation done by onStopRequest may take + // a while before the onStopRequest callback is made, so stop + // the timer now. + this.stopWatchdogTimer(); + } else { + this.touchWatchdogTimer(); + } + if (!this._startedSent) { + this.sendChromeEvent("update-download-started", { + total: aProgressMax + }); + this._startedSent = true; + } + + this.sendChromeEvent("update-download-progress", { + progress: aProgress, + total: aProgressMax + }); + }, + + onStatus: function UP_onStatus(aRequest, aUpdate, aStatus, aStatusArg) { } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([UpdatePrompt]); diff --git a/b2g/components/moz.build b/b2g/components/moz.build new file mode 100644 index 000000000..9290c5e2a --- /dev/null +++ b/b2g/components/moz.build @@ -0,0 +1,91 @@ +# -*- Mode: python; 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/. + +DIRS += ['test'] + +EXTRA_COMPONENTS += [ + 'AlertsService.js', + 'B2GAboutRedirector.js', + 'B2GAppMigrator.js', + 'B2GPresentationDevicePrompt.js', + 'BootstrapCommandLine.js', + 'ContentPermissionPrompt.js', + 'FilePicker.js', + 'FxAccountsUIGlue.js', + 'HelperAppDialog.js', + 'MailtoProtocolHandler.js', + 'OMAContentHandler.js', + 'PresentationRequestUIGlue.js', + 'ProcessGlobal.js', + 'SmsProtocolHandler.js', + 'SystemMessageInternal.js', + 'TelProtocolHandler.js', +] + +if CONFIG['OS_TARGET'] != 'Android': + EXTRA_COMPONENTS += [ + 'CommandLine.js', + 'OopCommandLine.js', + 'SimulatorScreen.js' + ] + +EXTRA_PP_COMPONENTS += [ + 'B2GComponents.manifest', +] + +if CONFIG['MOZ_B2G']: + EXTRA_COMPONENTS += [ + 'DirectoryProvider.js', + 'RecoveryService.js', + ] + +if CONFIG['MOZ_UPDATER']: + EXTRA_COMPONENTS += [ + 'UpdatePrompt.js', + ] + +EXTRA_JS_MODULES += [ + 'AboutServiceWorkers.jsm', + 'ActivityChannel.jsm', + 'AlertsHelper.jsm', + 'Bootstraper.jsm', + 'ContentRequestHelper.jsm', + 'DebuggerActors.js', + 'ErrorPage.jsm', + 'Frames.jsm', + 'FxAccountsMgmtService.jsm', + 'LogCapture.jsm', + 'LogParser.jsm', + 'LogShake.jsm', + 'OrientationChangeHandler.jsm', + 'SafeMode.jsm', + 'Screenshot.jsm', + 'SignInToWebsite.jsm', + 'SystemAppProxy.jsm', + 'TelURIParser.jsm', +] + +if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'gonk': + EXTRA_JS_MODULES += [ + 'GlobalSimulatorScreen.jsm' + ] + +XPIDL_SOURCES += [ + 'nsIGaiaChrome.idl', + 'nsISystemMessagesInternal.idl' +] + +XPIDL_MODULE = 'gaia_chrome' + +UNIFIED_SOURCES += [ + 'GaiaChrome.cpp' +] + +LOCAL_INCLUDES += [ + '/chrome' +] + +FINAL_LIBRARY = 'xul' diff --git a/b2g/components/nsIGaiaChrome.idl b/b2g/components/nsIGaiaChrome.idl new file mode 100644 index 000000000..8e66d8456 --- /dev/null +++ b/b2g/components/nsIGaiaChrome.idl @@ -0,0 +1,15 @@ +/* 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(92a18a98-ab5d-4d02-a024-bdbb3bc89ce1)] +interface nsIGaiaChrome : nsISupports +{ + void register(); +}; + +%{ C++ +#define GAIACHROME_CONTRACTID "@mozilla.org/b2g/gaia-chrome;1" +%} diff --git a/b2g/components/nsISystemMessagesInternal.idl b/b2g/components/nsISystemMessagesInternal.idl new file mode 100644 index 000000000..775ce0315 --- /dev/null +++ b/b2g/components/nsISystemMessagesInternal.idl @@ -0,0 +1,51 @@ +/* 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 "domstubs.idl" + +interface nsIURI; +interface nsIDOMWindow; +interface nsIMessageSender; + +// Implemented by the contract id @mozilla.org/system-message-internal;1 + +[scriptable, uuid(59b6beda-f911-4d47-a296-8c81e6abcfb9)] +interface nsISystemMessagesInternal : nsISupports +{ + /* + * Allow any internal user to send a message of a given type to a given page + * of an app. The message will be sent to all the registered pages of the app + * when |pageURI| is not specified. + * @param type The type of the message to be sent. + * @param message The message payload. + * @param pageURI The URI of the page that will be opened. Nullable. + * @param manifestURI The webapp's manifest URI. + * @param extra Extra opaque information that will be passed around in the observer + * notification to open the page. + * returns a Promise + */ + nsISupports sendMessage(in DOMString type, in jsval message, + in nsIURI pageURI, in nsIURI manifestURI, + [optional] in jsval extra); + + /* + * Allow any internal user to broadcast a message of a given type. + * The application that registers the message will be launched. + * @param type The type of the message to be sent. + * @param message The message payload. + * @param extra Extra opaque information that will be passed around in the observer + * notification to open the page. + * returns a Promise + */ + nsISupports broadcastMessage(in DOMString type, in jsval message, + [optional] in jsval extra); + + /* + * Registration of a page that wants to be notified of a message type. + * @param type The message type. + * @param pageURI The URI of the page that will be opened. + * @param manifestURI The webapp's manifest URI. + */ + void registerPage(in DOMString type, in nsIURI pageURI, in nsIURI manifestURI); +}; diff --git a/b2g/components/test/mochitest/SandboxPromptTest.html b/b2g/components/test/mochitest/SandboxPromptTest.html new file mode 100644 index 000000000..54f5fdd48 --- /dev/null +++ b/b2g/components/test/mochitest/SandboxPromptTest.html @@ -0,0 +1,57 @@ +<html> +<body> +<script> + +var actions = [ + { + permissions: ["video-capture"], + action: function() { + // invoke video-capture permission prompt + navigator.mozGetUserMedia({video: true}, function () {}, function () {}); + } + }, + { + permissions: ["audio-capture", "video-capture"], + action: function() { + // invoke audio-capture + video-capture permission prompt + navigator.mozGetUserMedia({audio: true, video: true}, function () {}, function () {}); + } + }, + { + permissions: ["audio-capture"], + action: function() { + // invoke audio-capture permission prompt + navigator.mozGetUserMedia({audio: true}, function () {}, function () {}); + } + }, + { + permissions: ["geolocation"], + action: function() { + // invoke geolocation permission prompt + navigator.geolocation.getCurrentPosition(function (pos) {}); + } + }, + { + permissions: ["desktop-notification"], + action: function() { + // invoke desktop-notification prompt + Notification.requestPermission(function (perm) {}); + } + }, +]; + +// The requested permissions are specified in query string. +var permissions = JSON.parse(decodeURIComponent(window.location.search.substring(1))); +for (var i = 0; i < actions.length; i++) { + if(permissions.length === actions[i].permissions.length && + permissions.every(function(permission) { + return actions[i].permissions.indexOf(permission) >= 0; + })) { + actions[i].action(); + break; + } +} + +</script> +</body> +</html> diff --git a/b2g/components/test/mochitest/filepicker_path_handler_chrome.js b/b2g/components/test/mochitest/filepicker_path_handler_chrome.js new file mode 100644 index 000000000..a175746cb --- /dev/null +++ b/b2g/components/test/mochitest/filepicker_path_handler_chrome.js @@ -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'; + +var Cc = Components.classes; +var Ci = Components.interfaces; + +// use ppmm to handle file-picker message. +var ppmm = Cc['@mozilla.org/parentprocessmessagemanager;1'] + .getService(Ci.nsIMessageListenerManager); + +var pickResult = null; + +function processPickMessage(message) { + let sender = message.target.QueryInterface(Ci.nsIMessageSender); + // reply FilePicker's message + sender.sendAsyncMessage('file-picked', pickResult); + // notify caller + sendAsyncMessage('file-picked-posted', { type: 'file-picked-posted' }); +} + +function updatePickResult(result) { + pickResult = result; + sendAsyncMessage('pick-result-updated', { type: 'pick-result-updated' }); +} + +ppmm.addMessageListener('file-picker', processPickMessage); +// use update-pick-result to change the expected pick result. +addMessageListener('update-pick-result', updatePickResult); diff --git a/b2g/components/test/mochitest/mochitest.ini b/b2g/components/test/mochitest/mochitest.ini new file mode 100644 index 000000000..97df32ea2 --- /dev/null +++ b/b2g/components/test/mochitest/mochitest.ini @@ -0,0 +1,28 @@ +[DEFAULT] +support-files = + permission_handler_chrome.js + SandboxPromptTest.html + filepicker_path_handler_chrome.js + screenshot_helper.js + systemapp_helper.js + presentation_prompt_handler_chrome.js + presentation_ui_glue_handler_chrome.js + +[test_filepicker_path.html] +skip-if = toolkit != "gonk" +[test_permission_deny.html] +skip-if = toolkit != "gonk" +[test_permission_gum_remember.html] +skip-if = true # Bug 1019572 - frequent timeouts +[test_sandbox_permission.html] +skip-if = toolkit != "gonk" +[test_screenshot.html] +skip-if = toolkit != "gonk" +[test_systemapp.html] +skip-if = toolkit != "gonk" +[test_presentation_device_prompt.html] +skip-if = toolkit != "gonk" +[test_permission_visibilitychange.html] +skip-if = toolkit != "gonk" +[test_presentation_request_ui_glue.html] +skip-if = toolkit != "gonk" diff --git a/b2g/components/test/mochitest/permission_handler_chrome.js b/b2g/components/test/mochitest/permission_handler_chrome.js new file mode 100644 index 000000000..9bf3e7819 --- /dev/null +++ b/b2g/components/test/mochitest/permission_handler_chrome.js @@ -0,0 +1,36 @@ +/* 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"; + +function debug(str) { + dump("CHROME PERMISSON HANDLER -- " + str + "\n"); +} + +var Cc = Components.classes; +var Ci = Components.interfaces; +var Cu = Components.utils; + +const { Services } = Cu.import("resource://gre/modules/Services.jsm"); +const { SystemAppProxy } = Cu.import("resource://gre/modules/SystemAppProxy.jsm"); + +var eventHandler = function(evt) { + if (!evt.detail || evt.detail.type !== "permission-prompt") { + return; + } + + sendAsyncMessage("permission-request", evt.detail); +}; + +SystemAppProxy.addEventListener("mozChromeEvent", eventHandler); + +// need to remove ChromeEvent listener after test finished. +addMessageListener("teardown", function() { + SystemAppProxy.removeEventListener("mozChromeEvent", eventHandler); +}); + +addMessageListener("permission-response", function(detail) { + SystemAppProxy._sendCustomEvent('mozContentEvent', detail); +}); + diff --git a/b2g/components/test/mochitest/presentation_prompt_handler_chrome.js b/b2g/components/test/mochitest/presentation_prompt_handler_chrome.js new file mode 100644 index 000000000..4407e58d2 --- /dev/null +++ b/b2g/components/test/mochitest/presentation_prompt_handler_chrome.js @@ -0,0 +1,94 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +'use strict'; + + function debug(str) { + dump('presentation_prompt_handler_chrome: ' + str + '\n'); + } + +var { classes: Cc, interfaces: Ci, utils: Cu } = Components; +const { XPCOMUtils } = Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +const { SystemAppProxy } = Cu.import('resource://gre/modules/SystemAppProxy.jsm'); + +const manager = Cc["@mozilla.org/presentation-device/manager;1"] + .getService(Ci.nsIPresentationDeviceManager); + +const prompt = Cc['@mozilla.org/presentation-device/prompt;1'] + .getService(Ci.nsIPresentationDevicePrompt); + +function TestPresentationDevice(options) { + this.id = options.id; + this.name = options.name; + this.type = options.type; +} + +TestPresentationDevice.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]), + establishSessionTransport: function() { + return null; + }, +}; + +function TestPresentationRequest(options) { + this.origin = options.origin; + this.requestURL = options.requestURL; +} + +TestPresentationRequest.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceRequest]), + select: function(device) { + let result = { + type: 'select', + device: { + id: device.id, + name: device.name, + type: device.type, + }, + }; + sendAsyncMessage('presentation-select-result', result); + }, + cancel: function() { + let result = { + type: 'cancel', + }; + sendAsyncMessage('presentation-select-result', result); + }, +}; + +var testDevice = null; + +addMessageListener('setup', function(device_options) { + testDevice = new TestPresentationDevice(device_options); + manager.QueryInterface(Ci.nsIPresentationDeviceListener).addDevice(testDevice); + sendAsyncMessage('setup-complete'); +}); + +var eventHandler = function(evt) { + if (!evt.detail || evt.detail.type !== 'presentation-select-device') { + return; + } + + sendAsyncMessage('presentation-select-device', evt.detail); +}; + +SystemAppProxy.addEventListener('mozChromeEvent', eventHandler); + +// need to remove ChromeEvent listener after test finished. +addMessageListener('teardown', function() { + if (testDevice) { + manager.removeDevice(testDevice); + } + SystemAppProxy.removeEventListener('mozChromeEvent', eventHandler); +}); + +addMessageListener('trigger-device-prompt', function(request_options) { + let request = new TestPresentationRequest(request_options); + prompt.promptDeviceSelection(request); +}); + +addMessageListener('presentation-select-response', function(detail) { + SystemAppProxy._sendCustomEvent('mozContentEvent', detail); +}); + diff --git a/b2g/components/test/mochitest/presentation_ui_glue_handler_chrome.js b/b2g/components/test/mochitest/presentation_ui_glue_handler_chrome.js new file mode 100644 index 000000000..fac16db6c --- /dev/null +++ b/b2g/components/test/mochitest/presentation_ui_glue_handler_chrome.js @@ -0,0 +1,32 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +'use strict'; + +var { classes: Cc, interfaces: Ci, utils: Cu } = Components; +const { XPCOMUtils } = Cu.import('resource://gre/modules/XPCOMUtils.jsm'); +const { SystemAppProxy } = Cu.import('resource://gre/modules/SystemAppProxy.jsm'); + +const glue = Cc["@mozilla.org/presentation/requestuiglue;1"] + .createInstance(Ci.nsIPresentationRequestUIGlue); + +SystemAppProxy.addEventListener('mozPresentationChromeEvent', function(aEvent) { + if (!aEvent.detail || aEvent.detail.type !== 'presentation-launch-receiver') { + return; + } + sendAsyncMessage('presentation-launch-receiver', aEvent.detail); +}); + +addMessageListener('trigger-ui-glue', function(aData) { + var promise = glue.sendRequest(aData.url, aData.sessionId); + promise.then(function(aFrame) { + sendAsyncMessage('iframe-resolved', aFrame); + }).catch(function() { + sendAsyncMessage('iframe-rejected'); + }); +}); + +addMessageListener('trigger-presentation-content-event', function(aDetail) { + SystemAppProxy._sendCustomEvent('mozPresentationContentEvent', aDetail); +}); diff --git a/b2g/components/test/mochitest/screenshot_helper.js b/b2g/components/test/mochitest/screenshot_helper.js new file mode 100644 index 000000000..0320a14c1 --- /dev/null +++ b/b2g/components/test/mochitest/screenshot_helper.js @@ -0,0 +1,40 @@ +var Cu = Components.utils; +var Ci = Components.interfaces; + +Cu.importGlobalProperties(['File']); + +const { Services } = Cu.import("resource://gre/modules/Services.jsm"); + +// Load a duplicated copy of the jsm to prevent messing with the currently running one +var scope = {}; +Services.scriptloader.loadSubScript("resource://gre/modules/Screenshot.jsm", scope); +const { Screenshot } = scope; + +var index = -1; +function next() { + index++; + if (index >= steps.length) { + assert.ok(false, "Shouldn't get here!"); + return; + } + try { + steps[index](); + } catch(ex) { + assert.ok(false, "Caught exception: " + ex); + } +} + +var steps = [ + function getScreenshot() { + let screenshot = Screenshot.get(); + assert.ok(screenshot instanceof File, + "Screenshot.get() returns a File"); + next(); + }, + + function endOfTest() { + sendAsyncMessage("finish"); + } +]; + +next(); diff --git a/b2g/components/test/mochitest/systemapp_helper.js b/b2g/components/test/mochitest/systemapp_helper.js new file mode 100644 index 000000000..768b221fe --- /dev/null +++ b/b2g/components/test/mochitest/systemapp_helper.js @@ -0,0 +1,173 @@ +var Cu = Components.utils; + +const { Services } = Cu.import("resource://gre/modules/Services.jsm"); + +// Load a duplicated copy of the jsm to prevent messing with the currently running one +var scope = {}; +Services.scriptloader.loadSubScript("resource://gre/modules/SystemAppProxy.jsm", scope); +const { SystemAppProxy } = scope; + +var frame; +var customEventTarget; + +var index = -1; +function next() { + index++; + if (index >= steps.length) { + assert.ok(false, "Shouldn't get here!"); + return; + } + try { + steps[index](); + } catch(ex) { + assert.ok(false, "Caught exception: " + ex); + } +} + +// Listen for events received by the system app document +// to ensure that we receive all of them, in an expected order and time +var isLoaded = false; +var isReady = false; +var n = 0; +function listener(event) { + if (!isLoaded) { + assert.ok(false, "Received event before the iframe is loaded"); + return; + } + n++; + if (n == 1) { + assert.equal(event.type, "mozChromeEvent"); + assert.equal(event.detail.name, "first"); + } else if (n == 2) { + assert.equal(event.type, "custom"); + assert.equal(event.detail.name, "second"); + + next(); // call checkEventPendingBeforeLoad + } else if (n == 3) { + if (!isReady) { + assert.ok(false, "Received event before the iframe is loaded"); + return; + } + + assert.equal(event.type, "custom"); + assert.equal(event.detail.name, "third"); + } else if (n == 4) { + if (!isReady) { + assert.ok(false, "Received event before the iframe is loaded"); + return; + } + + assert.equal(event.type, "mozChromeEvent"); + assert.equal(event.detail.name, "fourth"); + + next(); // call checkEventDispatching + } else if (n == 5) { + assert.equal(event.type, "custom"); + assert.equal(event.detail.name, "fifth"); + } else if (n === 6) { + assert.equal(event.type, "mozChromeEvent"); + assert.equal(event.detail.name, "sixth"); + } else if (n === 7) { + assert.equal(event.type, "custom"); + assert.equal(event.detail.name, "seventh"); + assert.equal(event.target, customEventTarget); + + next(); // call checkEventListening(); + } else { + assert.ok(false, "Unexpected event of type " + event.type); + } +} + + +var steps = [ + function earlyEvents() { + // Immediately try to send events + SystemAppProxy._sendCustomEvent("mozChromeEvent", { name: "first" }, true); + SystemAppProxy._sendCustomEvent("custom", { name: "second" }, true); + next(); + }, + + function createFrame() { + // Create a fake system app frame + let win = Services.wm.getMostRecentWindow("navigator:browser"); + let doc = win.document; + frame = doc.createElement("iframe"); + doc.documentElement.appendChild(frame); + + customEventTarget = frame.contentDocument.body; + + // Ensure that events are correctly sent to the frame. + // `listener` is going to call next() + frame.contentWindow.addEventListener("mozChromeEvent", listener); + frame.contentWindow.addEventListener("custom", listener); + + // Ensure that listener being registered before the system app is ready + // are correctly removed from the pending list + function removedListener() { + assert.ok(false, "Listener isn't correctly removed from the pending list"); + } + SystemAppProxy.addEventListener("mozChromeEvent", removedListener); + SystemAppProxy.removeEventListener("mozChromeEvent", removedListener); + + // Register it to the JSM + SystemAppProxy.registerFrame(frame); + assert.ok(true, "Frame created and registered"); + + frame.contentWindow.addEventListener("load", function onload() { + frame.contentWindow.removeEventListener("load", onload); + assert.ok(true, "Frame document loaded"); + + // Declare that the iframe is now loaded. + // That should dispatch early events + isLoaded = true; + SystemAppProxy.setIsLoaded(); + assert.ok(true, "Frame declared as loaded"); + + let gotFrame = SystemAppProxy.getFrame(); + assert.equal(gotFrame, frame, "getFrame returns the frame we passed"); + + // Once pending events are received, + // we will run checkEventDispatching from `listener` function + }); + + frame.setAttribute("src", "data:text/html,system app"); + }, + + function checkEventPendingBeforeLoad() { + // Frame is loaded but not ready, + // these events should queue before the System app is ready. + SystemAppProxy._sendCustomEvent("custom", { name: "third" }); + SystemAppProxy.dispatchEvent({ name: "fourth" }); + + isReady = true; + SystemAppProxy.setIsReady(); + // Once this 4th event is received, we will run checkEventDispatching + }, + + function checkEventDispatching() { + // Send events after the iframe is ready, + // they should be dispatched right away + SystemAppProxy._sendCustomEvent("custom", { name: "fifth" }); + SystemAppProxy.dispatchEvent({ name: "sixth" }); + SystemAppProxy._sendCustomEvent("custom", { name: "seventh" }, false, customEventTarget); + // Once this 7th event is received, we will run checkEventListening + }, + + function checkEventListening() { + SystemAppProxy.addEventListener("mozContentEvent", function onContentEvent(event) { + assert.equal(event.detail.name, "first-content", "received a system app event"); + SystemAppProxy.removeEventListener("mozContentEvent", onContentEvent); + + next(); + }); + let win = frame.contentWindow; + win.dispatchEvent(new win.CustomEvent("mozContentEvent", { detail: {name: "first-content"} })); + }, + + function endOfTest() { + frame.remove(); + sendAsyncMessage("finish"); + } +]; + +next(); diff --git a/b2g/components/test/mochitest/test_filepicker_path.html b/b2g/components/test/mochitest/test_filepicker_path.html new file mode 100644 index 000000000..92c00dc68 --- /dev/null +++ b/b2g/components/test/mochitest/test_filepicker_path.html @@ -0,0 +1,130 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=949944 +--> +<head> +<meta charset="utf-8"> +<title>Permission Prompt Test</title> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body onload="processTestCase()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=949944"> [B2G][Helix][Browser][Wallpaper] use new File([blob], filename) to return a blob with filename when picking</a> +<script type="application/javascript"> + +'use strict'; + +var testCases = [ + // case 1: returns blob with name + { pickedResult: { success: true, + result: { + type: 'text/plain', + blob: new Blob(['1234567890'], + { type: 'text/plain' }), + name: 'test1.txt' + } + }, + fileName: 'test1.txt' }, + // case 2: returns blob without name + { pickedResult: { success: true, + result: { + type: 'text/plain', + blob: new Blob(['1234567890'], + { type: 'text/plain' }) + } + }, + fileName: 'blob.txt' }, + // case 3: returns blob with full path name + { pickedResult: { success: true, + result: { + type: 'text/plain', + blob: new Blob(['1234567890'], + { type: 'text/plain' }), + name: '/full/path/test3.txt' + } + }, + fileName: 'test3.txt' }, + // case 4: returns blob relative path name + { pickedResult: { success: true, + result: { + type: 'text/plain', + blob: new Blob(['1234567890'], + { type: 'text/plain' }), + name: 'relative/path/test4.txt' + } + }, + fileName: 'test4.txt' }, + // case 5: returns file with name + { pickedResult: { success: true, + result: { + type: 'text/plain', + blob: new File(['1234567890'], + 'useless-name.txt', + { type: 'text/plain' }), + name: 'test5.txt' + } + }, + fileName: 'test5.txt'}, + // case 6: returns file without name. This case may fail because we + // need to make sure the File can be sent through + // sendAsyncMessage API. + { pickedResult: { success: true, + result: { + type: 'text/plain', + blob: new File(['1234567890'], + 'test6.txt', + { type: 'text/plain' }) + } + }, + fileName: 'test6.txt'} +]; + +var chromeJS = SimpleTest.getTestFileURL('filepicker_path_handler_chrome.js'); +var chromeScript = SpecialPowers.loadChromeScript(chromeJS); +var activeTestCase; + +chromeScript.addMessageListener('pick-result-updated', handleMessage); +chromeScript.addMessageListener('file-picked-posted', handleMessage); + +// handle messages returned from chromeScript +function handleMessage(data) { + var fileInput = document.getElementById('fileInput'); + switch (data.type) { + case 'pick-result-updated': + fileInput.click(); + break; + case 'file-picked-posted': + is(fileInput.value, activeTestCase.fileName, + 'File should be able to send through message.'); + processTestCase(); + break; + } +} + +function processTestCase() { + if (!testCases.length) { + SimpleTest.finish(); + return; + } + activeTestCase = testCases.shift(); + var expectedResult = activeTestCase.pickedResult; + if (navigator.userAgent.indexOf('Windows') > -1 && + expectedResult.result.name) { + // If we run at a window box, we need to translate the path from '/' to '\\' + var name = expectedResult.result.name; + name = name.replace('/', '\\'); + // If the name is an absolute path, we need to prepend drive letter. + if (name.startsWith('\\')) { + name = 'C:' + name; + } + // update the expected name. + expectedResult.result.name = name + } + chromeScript.sendAsyncMessage('update-pick-result', expectedResult); +} + +</script> +<input type="file" id="fileInput"> +</body> +</html> diff --git a/b2g/components/test/mochitest/test_permission_deny.html b/b2g/components/test/mochitest/test_permission_deny.html new file mode 100644 index 000000000..29c35bdec --- /dev/null +++ b/b2g/components/test/mochitest/test_permission_deny.html @@ -0,0 +1,83 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=981113 +--> +<head> + <meta charset="utf-8"> + <title>Permission Deny Test</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=981113">Test Permission Deny</a> +<script type="application/javascript;version=1.8"> + +'use strict'; + +SimpleTest.waitForExplicitFinish(); + +const PROMPT_ACTION = SpecialPowers.Ci.nsIPermissionManager.PROMPT_ACTION; + +var gUrl = SimpleTest.getTestFileURL('permission_handler_chrome.js'); +var gScript = SpecialPowers.loadChromeScript(gUrl); +var gTests = [ + { + 'video': true, + }, + { + 'audio': true, + 'video': true, + }, + { + 'audio': true, + }, +]; + +function runNext() { + if (gTests.length > 0) { + // Put the requested permission in query string + let requestedType = gTests.shift(); + info('getUserMedia for ' + JSON.stringify(requestedType)); + navigator.mozGetUserMedia(requestedType, function success() { + ok(false, 'unexpected success, permission request should be denied'); + runNext(); + }, function failure(err) { + is(err.name, 'SecurityError', 'expected permission denied'); + runNext(); + }); + } else { + info('test finished, teardown'); + gScript.sendAsyncMessage('teardown', ''); + gScript.destroy(); + SimpleTest.finish(); + } +} + +gScript.addMessageListener('permission-request', function(detail) { + let response = { + id: detail.id, + type: 'permission-deny', + remember: false, + }; + gScript.sendAsyncMessage('permission-response', response); +}); + +// Need to change camera permission from ALLOW to PROMPT, otherwise +// MediaManager will automatically allow video-only gUM request. +SpecialPowers.pushPermissions([ + {type: 'video-capture', allow: PROMPT_ACTION, context: document}, + {type: 'audio-capture', allow: PROMPT_ACTION, context: document}, + {type: 'camera', allow: PROMPT_ACTION, context: document}, + ], function() { + SpecialPowers.pushPrefEnv({ + 'set': [ + ['media.navigator.permission.disabled', false], + ] + }, runNext); + } +); +</script> +</pre> +</body> +</html> diff --git a/b2g/components/test/mochitest/test_permission_gum_remember.html b/b2g/components/test/mochitest/test_permission_gum_remember.html new file mode 100644 index 000000000..1ebfea1ca --- /dev/null +++ b/b2g/components/test/mochitest/test_permission_gum_remember.html @@ -0,0 +1,170 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=978660 +--> +<head> + <meta charset="utf-8"> + <title>gUM Remember Permission Test</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=978660">Test remembering gUM Permission</a> +<script type="application/javascript;version=1.8"> + +'use strict'; + +SimpleTest.waitForExplicitFinish(); + +const PROMPT_ACTION = SpecialPowers.Ci.nsIPermissionManager.PROMPT_ACTION; + +var gUrl = SimpleTest.getTestFileURL('permission_handler_chrome.js'); +var gScript = SpecialPowers.loadChromeScript(gUrl); +gScript.addMessageListener('permission-request', function(detail) { + ok(false, 'unexpected mozChromeEvent for permission prompt'); + let response = { + id: detail.id, + type: 'permission-deny', + remember: false, + }; + gScript.sendAsyncMessage('permission-response', response); +}); + +var gTests = [ + { + 'audio': true, + 'video': {facingMode: 'environment', required: ['facingMode']}, + }, + { + 'video': {facingMode: 'environment', required: ['facingMode']}, + }, + { + 'audio': true, + }, +]; + +function testGranted() { + info('test remember permission granted'); + return new Promise(function(resolve, reject) { + let steps = [].concat(gTests); + function nextStep() { + if (steps.length > 0) { + let requestedType = steps.shift(); + info('getUserMedia for ' + JSON.stringify(requestedType)); + navigator.mozGetUserMedia(requestedType, function success(stream) { + ok(true, 'expected gUM success'); + stream.stop(); + nextStep(); + }, function failure(err) { + ok(false, 'unexpected gUM fail: ' + err); + nextStep(); + }); + } else { + resolve(); + } + } + + SpecialPowers.pushPermissions([ + {type: 'video-capture', allow: true, context: document}, + {type: 'audio-capture', allow: true, context: document}, + ], nextStep); + }); +} + +function testDenied() { + info('test remember permission denied'); + return new Promise(function(resolve, reject) { + let steps = [].concat(gTests); + function nextStep() { + if (steps.length > 0) { + let requestedType = steps.shift(); + info('getUserMedia for ' + JSON.stringify(requestedType)); + navigator.mozGetUserMedia(requestedType, function success(stream) { + ok(false, 'unexpected gUM success'); + stream.stop(); + nextStep(); + }, function failure(err) { + ok(true, 'expected gUM fail: ' + err); + nextStep(); + }); + } else { + resolve(); + } + } + + SpecialPowers.pushPermissions([ + {type: 'video-capture', allow: false, context: document}, + {type: 'audio-capture', allow: false, context: document}, + ], nextStep); + }); +} + +function testPartialDeniedAudio() { + info('test remember permission partial denied: audio'); + return new Promise(function(resolve, reject) { + info('getUserMedia for video and audio'); + function nextStep() { + navigator.mozGetUserMedia({video: {facingMode: 'environment', required: ['facingMode']}, + audio: true}, function success(stream) { + ok(false, 'unexpected gUM success'); + stream.stop(); + resolve(); + }, function failure(err) { + ok(true, 'expected gUM fail: ' + err); + resolve(); + }); + } + + SpecialPowers.pushPermissions([ + {type: 'video-capture', allow: true, context: document}, + {type: 'audio-capture', allow: false, context: document}, + ], nextStep); + }); +} + +function testPartialDeniedVideo() { + info('test remember permission partial denied: video'); + return new Promise(function(resolve, reject) { + info('getUserMedia for video and audio'); + function nextStep() { + navigator.mozGetUserMedia({video: {facingMode: 'environment', required: ['facingMode']}, + audio: true}, function success(stream) { + ok(false, 'unexpected gUM success'); + stream.stop(); + resolve(); + }, function failure(err) { + ok(true, 'expected gUM fail: ' + err); + resolve(); + }); + } + + SpecialPowers.pushPermissions([ + {type: 'video-capture', allow: false, context: document}, + {type: 'audio-capture', allow: true, context: document}, + ], nextStep); + }); +} + +function runTests() { + testGranted() + .then(testDenied) + .then(testPartialDeniedAudio) + .then(testPartialDeniedVideo) + .then(function() { + info('test finished, teardown'); + gScript.sendAsyncMessage('teardown', ''); + gScript.destroy(); + SimpleTest.finish(); + }); +} + +SpecialPowers.pushPrefEnv({ + 'set': [ + ['media.navigator.permission.disabled', false], + ] +}, runTests); +</script> +</pre> +</body> +</html> diff --git a/b2g/components/test/mochitest/test_permission_visibilitychange.html b/b2g/components/test/mochitest/test_permission_visibilitychange.html new file mode 100644 index 000000000..cd5694b42 --- /dev/null +++ b/b2g/components/test/mochitest/test_permission_visibilitychange.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=951997 +--> +<head> + <meta charset="utf-8"> + <title>Permission Prompt Test</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1020179">Permission prompt visibilitychange test</a> +<script type="application/javascript;version=1.8"> + +"use strict"; + +var gUrl = SimpleTest.getTestFileURL("permission_handler_chrome.js"); +var gScript = SpecialPowers.loadChromeScript(gUrl); + +function testDone() { + gScript.sendAsyncMessage("teardown", ""); + gScript.destroy(); + SimpleTest.finish(); + alert("setVisible::true"); +} + +function runTest() { + navigator.geolocation.getCurrentPosition( + function (pos) { + ok(false, "unexpected success, permission request should be canceled"); + testDone(); + }, function (err) { + ok(true, "success, permission request is canceled"); + testDone(); + }); +} + +gScript.addMessageListener("permission-request", function (detail) { + info("got permission-request!!!!\n"); + alert("setVisible::false"); +}); + +// Add permissions to this app. We use ALLOW_ACTION here. The ContentPermissionPrompt +// should prompt for permission, not allow it without prompt. +SpecialPowers.pushPrefEnv({"set": [["media.navigator.permission.disabled", false]]}, + function() { + SpecialPowers.addPermission("geolocation", + SpecialPowers.Ci.nsIPermissionManager.PROMPT_ACTION, document); + runTest(); + }); + +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/b2g/components/test/mochitest/test_presentation_device_prompt.html b/b2g/components/test/mochitest/test_presentation_device_prompt.html new file mode 100644 index 000000000..9feeca795 --- /dev/null +++ b/b2g/components/test/mochitest/test_presentation_device_prompt.html @@ -0,0 +1,145 @@ +<!DOCTYPE HTML> +<html> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<head> + <meta charset="utf-8"> + <title>Test for Presentation Device Selection</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Test for Presentation Device Selection</a> +<script type="application/javascript;version=1.8"> + +'use strict'; + +SimpleTest.waitForExplicitFinish(); + +var contentEventHandler = null; + +var gUrl = SimpleTest.getTestFileURL('presentation_prompt_handler_chrome.js'); +var gScript = SpecialPowers.loadChromeScript(gUrl); + +function testSetup() { + info('setup for device selection'); + return new Promise(function(resolve, reject) { + let device = { + id: 'test-id', + name: 'test-name', + type: 'test-type', + }; + gScript.addMessageListener('setup-complete', function() { + resolve(device); + }); + gScript.sendAsyncMessage('setup', device); + }); +} + +function testSelected(device) { + info('test device selected by user'); + return new Promise(function(resolve, reject) { + let request = { + origin: 'test-origin', + requestURL: 'test-requestURL', + }; + + gScript.addMessageListener('presentation-select-device', function contentEventHandler(detail) { + gScript.removeMessageListener('presentation-select-device', contentEventHandler); + ok(true, 'receive user prompt for device selection'); + is(detail.origin, request.origin, 'expected origin'); + is(detail.requestURL, request.requestURL, 'expected requestURL'); + let response = { + id: detail.id, + type: 'presentation-select-result', + deviceId: device.id, + }; + gScript.sendAsyncMessage('presentation-select-response', response); + + gScript.addMessageListener('presentation-select-result', function resultHandler(result) { + gScript.removeMessageListener('presentation-select-result', resultHandler); + is(result.type, 'select', 'expect device selected'); + is(result.device.id, device.id, 'expected device id'); + is(result.device.name, device.name, 'expected device name'); + is(result.device.type, device.type, 'expected devcie type'); + resolve(); + }); + }); + + gScript.sendAsyncMessage('trigger-device-prompt', request); + }); +} + +function testSelectedNotExisted() { + info('test selected device doesn\'t exist'); + return new Promise(function(resolve, reject) { + gScript.addMessageListener('presentation-select-device', function contentEventHandler(detail) { + gScript.removeMessageListener('presentation-select-device', contentEventHandler); + ok(true, 'receive user prompt for device selection'); + let response = { + id: detail.id, + type: 'presentation-select-deny', + deviceId: undefined, // simulate device Id that doesn't exist + }; + gScript.sendAsyncMessage('presentation-select-response', response); + + gScript.addMessageListener('presentation-select-result', function resultHandler(result) { + gScript.removeMessageListener('presentation-select-result', resultHandler); + is(result.type, 'cancel', 'expect user cancel'); + resolve(); + }); + }); + + let request = { + origin: 'test-origin', + requestURL: 'test-requestURL', + }; + gScript.sendAsyncMessage('trigger-device-prompt', request); + }); +} + +function testDenied() { + info('test denial of device selection'); + return new Promise(function(resolve, reject) { + gScript.addMessageListener('presentation-select-device', function contentEventHandler(detail) { + gScript.removeMessageListener('presentation-select-device', contentEventHandler); + ok(true, 'receive user prompt for device selection'); + let response = { + id: detail.id, + type: 'presentation-select-deny', + }; + gScript.sendAsyncMessage('presentation-select-response', response); + + gScript.addMessageListener('presentation-select-result', function resultHandler(result) { + gScript.removeMessageListener('presentation-select-result', resultHandler); + is(result.type, 'cancel', 'expect user cancel'); + resolve(); + }); + }); + + let request = { + origin: 'test-origin', + requestURL: 'test-requestURL', + }; + gScript.sendAsyncMessage('trigger-device-prompt', request); + }); +} + +function runTests() { + testSetup() + .then(testSelected) + .then(testSelectedNotExisted) + .then(testDenied) + .then(function() { + info('test finished, teardown'); + gScript.sendAsyncMessage('teardown'); + gScript.destroy(); + SimpleTest.finish(); + }); +} + +window.addEventListener('load', runTests); +</script> +</pre> +</body> +</html> diff --git a/b2g/components/test/mochitest/test_presentation_request_ui_glue.html b/b2g/components/test/mochitest/test_presentation_request_ui_glue.html new file mode 100644 index 000000000..29ac37221 --- /dev/null +++ b/b2g/components/test/mochitest/test_presentation_request_ui_glue.html @@ -0,0 +1,105 @@ +<!DOCTYPE HTML> +<html> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<head> + <meta charset="utf-8"> + <title>Test for Presentation UI Glue</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Test for Presentation UI Glue</a> +<script type="application/javascript;version=1.8"> + +'use strict'; + +SimpleTest.waitForExplicitFinish(); + +var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL('presentation_ui_glue_handler_chrome.js')); + +var obs = SpecialPowers.Cc["@mozilla.org/observer-service;1"] + .getService(SpecialPowers.Ci.nsIObserverService); + +var url = 'http://example.com'; +var sessionId = 'sessionId'; + +function testLaunchReceiver() { + return new Promise(function(aResolve, aReject) { + gScript.addMessageListener('presentation-launch-receiver', function launchReceiverHandler(aDetail) { + gScript.removeMessageListener('presentation-launch-receiver', launchReceiverHandler); + ok(true, "A presentation-launch-receiver mozPresentationChromeEvent should be received."); + is(aDetail.url, url, "Url should be the same."); + is(aDetail.id, sessionId, "Session ID should be the same."); + + aResolve(); + }); + + gScript.sendAsyncMessage('trigger-ui-glue', + { url: url, + sessionId : sessionId }); + }); +} + +function testReceiverLaunched() { + return new Promise(function(aResolve, aReject) { + gScript.addMessageListener('iframe-resolved', function iframeResolvedHandler(aFrame) { + gScript.removeMessageListener('iframe-resolved', iframeResolvedHandler); + ok(true, "The promise should be resolved."); + + aResolve(); + }); + + var iframe = document.createElement('iframe'); + iframe.setAttribute('remote', 'true'); + iframe.setAttribute('mozbrowser', 'true'); + iframe.setAttribute('src', 'http://example.com'); + document.body.appendChild(iframe); + + gScript.sendAsyncMessage('trigger-presentation-content-event', + { type: 'presentation-receiver-launched', + id: sessionId, + frame: iframe }); + }); +} + +function testLaunchError() { + return new Promise(function(aResolve, aReject) { + gScript.addMessageListener('presentation-launch-receiver', function launchReceiverHandler(aDetail) { + gScript.removeMessageListener('presentation-launch-receiver', launchReceiverHandler); + ok(true, "A presentation-launch-receiver mozPresentationChromeEvent should be received."); + is(aDetail.url, url, "Url should be the same."); + is(aDetail.id, sessionId, "Session ID should be the same."); + + gScript.addMessageListener('iframe-rejected', function iframeRejectedHandler() { + gScript.removeMessageListener('iframe-rejected', iframeRejectedHandler); + ok(true, "The promise should be rejected."); + aResolve(); + }); + + gScript.sendAsyncMessage('trigger-presentation-content-event', + { type: 'presentation-receiver-permission-denied', + id: sessionId }); + }); + + gScript.sendAsyncMessage('trigger-ui-glue', + { url: url, + sessionId : sessionId }); + }); +} + +function runTests() { + testLaunchReceiver() + .then(testReceiverLaunched) + .then(testLaunchError) + .then(function() { + info('test finished, teardown'); + gScript.destroy(); + SimpleTest.finish(); + }); +} + +window.addEventListener('load', runTests); +</script> +</body> +</html> diff --git a/b2g/components/test/mochitest/test_sandbox_permission.html b/b2g/components/test/mochitest/test_sandbox_permission.html new file mode 100644 index 000000000..cd13599a3 --- /dev/null +++ b/b2g/components/test/mochitest/test_sandbox_permission.html @@ -0,0 +1,104 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=951997 +--> +<head> + <meta charset="utf-8"> + <title>Permission Prompt Test</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=951997">Permission prompt web content test</a> +<script type="application/javascript;version=1.8"> + +"use strict"; + +const APP_URL = "SandboxPromptTest.html"; + +var iframe; +var gUrl = SimpleTest.getTestFileURL("permission_handler_chrome.js"); +var gScript = SpecialPowers.loadChromeScript(gUrl); +var gResult = [ + { + "video-capture": ["back"], + }, + { + "audio-capture": [""], + "video-capture": ["back"], + }, + { + "audio-capture": [""], + }, + { + "geolocation": [], + }, + { + "desktop-notification": [], + } +]; + +function runNext() { + if (gResult.length > 0) { + // Put the requested permission in query string + let requestedPermission = JSON.stringify(Object.keys(gResult[0])); + info('request permissions for ' + requestedPermission); + iframe.src = APP_URL + '?' + encodeURIComponent(requestedPermission); + } else { + info('test finished, teardown'); + gScript.sendAsyncMessage("teardown", ""); + gScript.destroy(); + SimpleTest.finish(); + } +} + +// Create a sanbox iframe. +function loadBrowser() { + iframe = document.createElement("iframe"); + SpecialPowers.wrap(iframe).mozbrowser = true; + iframe.src = 'about:blank'; + document.body.appendChild(iframe); + + iframe.addEventListener("load", function onLoad() { + iframe.removeEventListener("load", onLoad); + runNext(); + }); +} + +gScript.addMessageListener("permission-request", function (detail) { + let permissions = detail.permissions; + let expectedValue = gResult.shift(); + let permissionTypes = Object.keys(permissions); + + is(permissionTypes.length, Object.keys(expectedValue).length, "expected number of permissions"); + + for (let type of permissionTypes) { + ok(expectedValue.hasOwnProperty(type), "expected permission type"); + for (let i in permissions[type]) { + is(permissions[type][i], expectedValue[type][i], "expected permission option"); + } + } + runNext(); +}); + +// Add permissions to this app. We use ALLOW_ACTION here. The ContentPermissionPrompt +// should prompt for permission, not allow it without prompt. +SpecialPowers.pushPrefEnv({"set": [["media.navigator.permission.disabled", false]]}, + function() { + SpecialPowers.addPermission('video-capture', + SpecialPowers.Ci.nsIPermissionManager.ALLOW_ACTION, document); + SpecialPowers.addPermission('audio-capture', + SpecialPowers.Ci.nsIPermissionManager.ALLOW_ACTION, document); + SpecialPowers.addPermission('geolocation', + SpecialPowers.Ci.nsIPermissionManager.ALLOW_ACTION, document); + SpecialPowers.addPermission('desktop-notification', + SpecialPowers.Ci.nsIPermissionManager.ALLOW_ACTION, document); + loadBrowser(); + }); + +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> diff --git a/b2g/components/test/mochitest/test_screenshot.html b/b2g/components/test/mochitest/test_screenshot.html new file mode 100644 index 000000000..d2eeb8d48 --- /dev/null +++ b/b2g/components/test/mochitest/test_screenshot.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1136784 +--> +<head> + <meta charset="utf-8"> + <title>Screenshot Test</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1136784">Screenshot.jsm</a> +<script type="application/javascript"> + +"use strict"; + +var gUrl = SimpleTest.getTestFileURL("screenshot_helper.js"); +var gScript = SpecialPowers.loadChromeScript(gUrl); + +SimpleTest.waitForExplicitFinish(); +gScript.addMessageListener("finish", function () { + SimpleTest.ok(true, "chrome test script finished"); + gScript.destroy(); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/b2g/components/test/mochitest/test_systemapp.html b/b2g/components/test/mochitest/test_systemapp.html new file mode 100644 index 000000000..450094a50 --- /dev/null +++ b/b2g/components/test/mochitest/test_systemapp.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=963239 +--> +<head> + <meta charset="utf-8"> + <title>SystemAppProxy Test</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=963239">SystemAppProxy.jsm</a> +<script type="application/javascript"> + +"use strict"; + +var gUrl = SimpleTest.getTestFileURL("systemapp_helper.js"); +var gScript = SpecialPowers.loadChromeScript(gUrl); + +SimpleTest.waitForExplicitFinish(); +gScript.addMessageListener("finish", function () { + SimpleTest.ok(true, "chrome test script finished"); + gScript.destroy(); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/b2g/components/test/moz.build b/b2g/components/test/moz.build new file mode 100644 index 000000000..387e3b811 --- /dev/null +++ b/b2g/components/test/moz.build @@ -0,0 +1,8 @@ +# -*- Mode: python; 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/. + +XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini'] +MOCHITEST_MANIFESTS += ['mochitest/mochitest.ini'] diff --git a/b2g/components/test/unit/data/test_logger_file b/b2g/components/test/unit/data/test_logger_file Binary files differnew file mode 100644 index 000000000..b1ed7f10a --- /dev/null +++ b/b2g/components/test/unit/data/test_logger_file diff --git a/b2g/components/test/unit/head_identity.js b/b2g/components/test/unit/head_identity.js new file mode 100644 index 000000000..604a77284 --- /dev/null +++ b/b2g/components/test/unit/head_identity.js @@ -0,0 +1,159 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var Ci = Components.interfaces; +var Cu = Components.utils; + +// The following boilerplate makes sure that XPCOM calls +// that use the profile directory work. + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "MinimalIDService", + "resource://gre/modules/identity/MinimalIdentity.jsm", + "IdentityService"); + +XPCOMUtils.defineLazyModuleGetter(this, + "Logger", + "resource://gre/modules/identity/LogUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, + "uuidGenerator", + "@mozilla.org/uuid-generator;1", + "nsIUUIDGenerator"); + +const TEST_URL = "https://myfavoriteflan.com"; +const TEST_USER = "uumellmahaye1969@hotmail.com"; +const TEST_PRIVKEY = "i-am-a-secret"; +const TEST_CERT = "i~like~pie"; + +// The following are utility functions for Identity testing + +function log(...aMessageArgs) { + Logger.log.apply(Logger, ["test"].concat(aMessageArgs)); +} + +function partial(fn) { + let args = Array.prototype.slice.call(arguments, 1); + return function() { + return fn.apply(this, args.concat(Array.prototype.slice.call(arguments))); + }; +} + +function uuid() { + return uuidGenerator.generateUUID().toString(); +} + +// create a mock "doc" object, which the Identity Service +// uses as a pointer back into the doc object +function mockDoc(aParams, aDoFunc) { + let mockedDoc = {}; + mockedDoc.id = uuid(); + + // Properties of aParams may include loggedInUser + Object.keys(aParams).forEach(function(param) { + mockedDoc[param] = aParams[param]; + }); + + // the origin is set inside nsDOMIdentity by looking at the + // document.nodePrincipal.origin. Here we, we must satisfy + // ourselves with pretending. + mockedDoc.origin = "https://jedp.gov"; + + mockedDoc['do'] = aDoFunc; + mockedDoc.doReady = partial(aDoFunc, 'ready'); + mockedDoc.doLogin = partial(aDoFunc, 'login'); + mockedDoc.doLogout = partial(aDoFunc, 'logout'); + mockedDoc.doError = partial(aDoFunc, 'error'); + mockedDoc.doCancel = partial(aDoFunc, 'cancel'); + mockedDoc.doCoffee = partial(aDoFunc, 'coffee'); + + return mockedDoc; +} + +// create a mock "pipe" object that would normally communicate +// messages up to gaia (either the trusty ui or the hidden iframe), +// and convey messages back down from gaia to the controller through +// the message callback. + +// The mock receiving pipe simulates gaia which, after receiving messages +// through the pipe, will call back with instructions to invoke +// certain methods. It mocks what comes back from the other end of +// the pipe. +function mockReceivingPipe() { + let MockedPipe = { + communicate: function(aRpOptions, aGaiaOptions, aMessageCallback) { + switch (aGaiaOptions.message) { + case "identity-delegate-watch": + aMessageCallback({json: {method: "ready"}}); + break; + case "identity-delegate-request": + aMessageCallback({json: {method: "login", assertion: TEST_CERT}}); + break; + case "identity-delegate-logout": + aMessageCallback({json: {method: "logout"}}); + break; + default: + throw("what the what?? " + aGaiaOptions.message); + break; + } + } + }; + return MockedPipe; +} + +// The mock sending pipe lets us test what's actually getting put in the +// pipe. +function mockSendingPipe(aMessageCallback) { + let MockedPipe = { + communicate: function(aRpOptions, aGaiaOptions, aDummyCallback) { + aMessageCallback(aRpOptions, aGaiaOptions); + } + }; + return MockedPipe; +} + +// mimicking callback funtionality for ease of testing +// this observer auto-removes itself after the observe function +// is called, so this is meant to observe only ONE event. +function makeObserver(aObserveTopic, aObserveFunc) { + let observer = { + // nsISupports provides type management in C++ + // nsIObserver is to be an observer + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]), + + observe: function (aSubject, aTopic, aData) { + if (aTopic == aObserveTopic) { + Services.obs.removeObserver(observer, aObserveTopic); + aObserveFunc(aSubject, aTopic, aData); + } + } + }; + + Services.obs.addObserver(observer, aObserveTopic, false); +} + +// a hook to set up the ID service with an identity with keypair and all +// when ready, invoke callback with the identity. It's there if we need it. +function setup_test_identity(identity, cert, cb) { + cb(); +} + +// takes a list of functions and returns a function that +// when called the first time, calls the first func, +// then the next time the second, etc. +function call_sequentially() { + let numCalls = 0; + let funcs = arguments; + + return function() { + if (!funcs[numCalls]) { + let argString = Array.prototype.slice.call(arguments).join(","); + do_throw("Too many calls: " + argString); + return; + } + funcs[numCalls].apply(funcs[numCalls],arguments); + numCalls += 1; + }; +} diff --git a/b2g/components/test/unit/head_logshake_gonk.js b/b2g/components/test/unit/head_logshake_gonk.js new file mode 100644 index 000000000..e94234f1f --- /dev/null +++ b/b2g/components/test/unit/head_logshake_gonk.js @@ -0,0 +1,58 @@ +/** + * Boostrap LogShake's tests that need gonk support. + * This is creating a fake sdcard for LogShake tests and importing LogShake and + * osfile + */ + +/* jshint moz: true */ +/* global Components, LogCapture, LogShake, ok, add_test, run_next_test, dump, + do_get_profile, OS, volumeService, equal, XPCOMUtils */ +/* exported setup_logshake_mocks */ + +/* disable use strict warning */ +/* jshint -W097 */ + +"use strict"; + +var Cu = Components.utils; +var Ci = Components.interfaces; +var Cc = Components.classes; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/osfile.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "volumeService", + "@mozilla.org/telephony/volume-service;1", + "nsIVolumeService"); + +var sdcard; + +function setup_logshake_mocks() { + do_get_profile(); + setup_fs(); +} + +function setup_fs() { + OS.File.makeDir("/data/local/tmp/sdcard/", {from: "/data"}).then(function() { + setup_sdcard(); + }); +} + +function setup_sdcard() { + let volName = "sdcard"; + let mountPoint = "/data/local/tmp/sdcard"; + volumeService.createFakeVolume(volName, mountPoint); + + let vol = volumeService.getVolumeByName(volName); + ok(vol, "volume shouldn't be null"); + equal(volName, vol.name, "name"); + equal(Ci.nsIVolume.STATE_MOUNTED, vol.state, "state"); + + ensure_sdcard(); +} + +function ensure_sdcard() { + sdcard = volumeService.getVolumeByName("sdcard").mountPoint; + ok(sdcard, "Should have a valid sdcard mountpoint"); + run_next_test(); +} diff --git a/b2g/components/test/unit/test_aboutserviceworkers.js b/b2g/components/test/unit/test_aboutserviceworkers.js new file mode 100644 index 000000000..d1a7d41aa --- /dev/null +++ b/b2g/components/test/unit/test_aboutserviceworkers.js @@ -0,0 +1,142 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var {utils: Cu} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "AboutServiceWorkers", + "resource://gre/modules/AboutServiceWorkers.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "gServiceWorkerManager", + "@mozilla.org/serviceworkers/manager;1", + "nsIServiceWorkerManager"); + +const CHROME_MSG = "mozAboutServiceWorkersChromeEvent"; + +const ORIGINAL_SENDRESULT = AboutServiceWorkers.sendResult; +const ORIGINAL_SENDERROR = AboutServiceWorkers.sendError; + +do_get_profile(); + +var mockSendResult = (aId, aResult) => { + let msg = { + id: aId, + result: aResult + }; + Services.obs.notifyObservers({wrappedJSObject: msg}, CHROME_MSG, null); +}; + +var mockSendError = (aId, aError) => { + let msg = { + id: aId, + result: aError + }; + Services.obs.notifyObservers({wrappedJSObject: msg}, CHROME_MSG, null); +}; + +function attachMocks() { + AboutServiceWorkers.sendResult = mockSendResult; + AboutServiceWorkers.sendError = mockSendError; +} + +function restoreMocks() { + AboutServiceWorkers.sendResult = ORIGINAL_SENDRESULT; + AboutServiceWorkers.sendError = ORIGINAL_SENDERROR; +} + +do_register_cleanup(restoreMocks); + +function run_test() { + run_next_test(); +} + +/** + * "init" tests + */ +[ +// Pref disabled, no registrations +{ + prefEnabled: false, + expectedMessage: { + id: Date.now(), + result: { + enabled: false, + registrations: [] + } + } +}, +// Pref enabled, no registrations +{ + prefEnabled: true, + expectedMessage: { + id: Date.now(), + result: { + enabled: true, + registrations: [] + } + } +}].forEach(test => { + add_test(function() { + Services.prefs.setBoolPref("dom.serviceWorkers.enabled", test.prefEnabled); + + let id = test.expectedMessage.id; + + function onMessage(subject, topic, data) { + let message = subject.wrappedJSObject; + let expected = test.expectedMessage; + + do_check_true(message.id, "Message should have id"); + do_check_eq(message.id, test.expectedMessage.id, + "Id should be the expected one"); + do_check_eq(message.result.enabled, expected.result.enabled, + "Pref should be disabled"); + do_check_true(message.result.registrations, "Registrations should exist"); + do_check_eq(message.result.registrations.length, + expected.result.registrations.length, + "Registrations length should be the expected one"); + + Services.obs.removeObserver(onMessage, CHROME_MSG); + + run_next_test(); + } + + Services.obs.addObserver(onMessage, CHROME_MSG, false); + + attachMocks(); + + AboutServiceWorkers.handleEvent({ detail: { + id: id, + name: "init" + }}); + }); +}); + +/** + * ServiceWorkerManager tests. + */ + +// We cannot register a sw via ServiceWorkerManager cause chrome +// registrations are not allowed. +// All we can do for now is to test the interface of the swm. +add_test(function test_swm() { + do_check_true(gServiceWorkerManager, "SWM exists"); + do_check_true(gServiceWorkerManager.getAllRegistrations, + "SWM.getAllRegistrations exists"); + do_check_true(typeof gServiceWorkerManager.getAllRegistrations == "function", + "SWM.getAllRegistrations is a function"); + do_check_true(gServiceWorkerManager.propagateSoftUpdate, + "SWM.propagateSoftUpdate exists"); + do_check_true(typeof gServiceWorkerManager.propagateSoftUpdate == "function", + + "SWM.propagateSoftUpdate is a function"); + do_check_true(gServiceWorkerManager.propagateUnregister, + "SWM.propagateUnregister exists"); + do_check_true(typeof gServiceWorkerManager.propagateUnregister == "function", + "SWM.propagateUnregister exists"); + + run_next_test(); +}); diff --git a/b2g/components/test/unit/test_bug793310.js b/b2g/components/test/unit/test_bug793310.js new file mode 100644 index 000000000..2bdb8252e --- /dev/null +++ b/b2g/components/test/unit/test_bug793310.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function run_test() { + Components.utils.import("resource:///modules/TelURIParser.jsm") + + // global-phone-number + do_check_eq(TelURIParser.parseURI('tel', 'tel:+1234'), '+1234'); + + // global-phone-number => white space separator + do_check_eq(TelURIParser.parseURI('tel', 'tel:+123 456 789'), '+123 456 789'); + + // global-phone-number => ignored chars + do_check_eq(TelURIParser.parseURI('tel', 'tel:+1234_123'), '+1234'); + + // global-phone-number => visualSeparator + digits + do_check_eq(TelURIParser.parseURI('tel', 'tel:+-.()1234567890'), '+-.()1234567890'); + + // local-phone-number + do_check_eq(TelURIParser.parseURI('tel', 'tel:1234'), '1234'); + + // local-phone-number => visualSeparator + digits + dtmfDigits + pauseCharacter + do_check_eq(TelURIParser.parseURI('tel', 'tel:-.()1234567890ABCDpw'), '-.()1234567890ABCDpw'); + + // local-phone-number => visualSeparator + digits + dtmfDigits + pauseCharacter + ignored chars + do_check_eq(TelURIParser.parseURI('tel', 'tel:-.()1234567890ABCDpw_'), '-.()1234567890ABCDpw'); + + // local-phone-number => isdn-subaddress + do_check_eq(TelURIParser.parseURI('tel', 'tel:123;isub=123'), '123'); + + // local-phone-number => post-dial + do_check_eq(TelURIParser.parseURI('tel', 'tel:123;postd=123'), '123'); + + // local-phone-number => prefix + do_check_eq(TelURIParser.parseURI('tel', 'tel:123;phone-context=+0321'), '+0321123'); + + // local-phone-number => isdn-subaddress + post-dial + prefix + do_check_eq(TelURIParser.parseURI('tel', 'tel:123;isub=123;postd=123;phone-context=+0321'), '+0321123'); +} diff --git a/b2g/components/test/unit/test_bug832946.js b/b2g/components/test/unit/test_bug832946.js new file mode 100644 index 000000000..4ddbd4280 --- /dev/null +++ b/b2g/components/test/unit/test_bug832946.js @@ -0,0 +1,18 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function run_test() { + Components.utils.import("resource:///modules/TelURIParser.jsm") + + // blocked numbers + do_check_eq(TelURIParser.parseURI('tel', 'tel:#1234*'), null); + do_check_eq(TelURIParser.parseURI('tel', 'tel:*1234#'), null); + do_check_eq(TelURIParser.parseURI('tel', 'tel:*1234*'), null); + do_check_eq(TelURIParser.parseURI('tel', 'tel:#1234#'), null); + do_check_eq(TelURIParser.parseURI('tel', 'tel:*#*#7780#*#*'), null); + do_check_eq(TelURIParser.parseURI('tel', 'tel:*1234AB'), null); + + // white list + do_check_eq(TelURIParser.parseURI('tel', 'tel:*1234'), '*1234'); + do_check_eq(TelURIParser.parseURI('tel', 'tel:#1234'), '#1234'); +} diff --git a/b2g/components/test/unit/test_fxaccounts.js b/b2g/components/test/unit/test_fxaccounts.js new file mode 100644 index 000000000..5de0d6565 --- /dev/null +++ b/b2g/components/test/unit/test_fxaccounts.js @@ -0,0 +1,212 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var {utils: Cu} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://services-common/utils.js"); +Cu.import("resource://testing-common/httpd.js"); + +XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsMgmtService", + "resource://gre/modules/FxAccountsMgmtService.jsm", + "FxAccountsMgmtService"); + +XPCOMUtils.defineLazyModuleGetter(this, "FxAccountsManager", + "resource://gre/modules/FxAccountsManager.jsm"); + +// At end of test, restore original state +const ORIGINAL_AUTH_URI = Services.prefs.getCharPref("identity.fxaccounts.auth.uri"); +var { SystemAppProxy } = Cu.import("resource://gre/modules/FxAccountsMgmtService.jsm"); +const ORIGINAL_SENDCUSTOM = SystemAppProxy._sendCustomEvent; +do_register_cleanup(function() { + Services.prefs.setCharPref("identity.fxaccounts.auth.uri", ORIGINAL_AUTH_URI); + SystemAppProxy._sendCustomEvent = ORIGINAL_SENDCUSTOM; + Services.prefs.clearUserPref("identity.fxaccounts.skipDeviceRegistration"); +}); + +// Make profile available so that fxaccounts can store user data +do_get_profile(); + +// Mock the system app proxy; make message passing possible +var mockSendCustomEvent = function(aEventName, aMsg) { + Services.obs.notifyObservers({wrappedJSObject: aMsg}, aEventName, null); +}; + +function run_test() { + run_next_test(); +} + +add_task(function test_overall() { + // FxA device registration throws from this context + Services.prefs.setBoolPref("identity.fxaccounts.skipDeviceRegistration", true); + + do_check_neq(FxAccountsMgmtService, null); +}); + +// Check that invalid email capitalization is corrected on signIn. +// https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#post-v1accountlogin +add_test(function test_invalidEmailCase_signIn() { + do_test_pending(); + let clientEmail = "greta.garbo@gmail.com"; + let canonicalEmail = "Greta.Garbo@gmail.COM"; + let attempts = 0; + + function writeResp(response, msg) { + if (typeof msg === "object") { + msg = JSON.stringify(msg); + } + response.bodyOutputStream.write(msg, msg.length); + } + + // Mock of the fxa accounts auth server, reproducing the behavior of + // /account/login when email capitalization is incorrect on signIn. + let server = httpd_setup({ + "/account/login": function(request, response) { + response.setHeader("Content-Type", "application/json"); + attempts += 1; + + // Ensure we don't get in an endless loop + if (attempts > 2) { + response.setStatusLine(request.httpVersion, 429, "Sorry, you had your chance"); + writeResp(response, {}); + return; + } + + let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream); + let jsonBody = JSON.parse(body); + let email = jsonBody.email; + + // The second time through, the accounts client will call the api with + // the correct email capitalization. + if (email == canonicalEmail) { + response.setStatusLine(request.httpVersion, 200, "Yay"); + writeResp(response, { + uid: "your-uid", + sessionToken: "your-sessionToken", + keyFetchToken: "your-keyFetchToken", + verified: true, + authAt: 1392144866, + }); + return; + } + + // If the client has the wrong case on the email, we return a 400, with + // the capitalization of the email as saved in the accounts database. + response.setStatusLine(request.httpVersion, 400, "Incorrect email case"); + writeResp(response, { + code: 400, + errno: 120, + error: "Incorrect email case", + email: canonicalEmail, + }); + return; + }, + }); + + // Point the FxAccountsClient's hawk rest request client to the mock server + Services.prefs.setCharPref("identity.fxaccounts.auth.uri", server.baseURI); + + // FxA device registration throws from this context + Services.prefs.setBoolPref("identity.fxaccounts.skipDeviceRegistration", true); + + // Receive a mozFxAccountsChromeEvent message + function onMessage(subject, topic, data) { + let message = subject.wrappedJSObject; + + switch (message.id) { + // When we signed in as "Greta.Garbo", the server should have told us + // that the proper capitalization is really "greta.garbo". Call + // getAccounts to get the signed-in user and ensure that the + // capitalization is correct. + case "signIn": + FxAccountsMgmtService.handleEvent({ + detail: { + id: "getAccounts", + data: { + method: "getAccounts", + } + } + }); + break; + + // Having initially signed in as "Greta.Garbo", getAccounts should show + // us that the signed-in user has the properly-capitalized email, + // "greta.garbo". + case "getAccounts": + Services.obs.removeObserver(onMessage, "mozFxAccountsChromeEvent"); + + do_check_eq(message.data.email, canonicalEmail); + + do_test_finished(); + server.stop(run_next_test); + break; + + // We should not receive any other mozFxAccountsChromeEvent messages + default: + do_throw("wat!"); + break; + } + } + + Services.obs.addObserver(onMessage, "mozFxAccountsChromeEvent", false); + + SystemAppProxy._sendCustomEvent = mockSendCustomEvent; + + // Trigger signIn using an email with incorrect capitalization + FxAccountsMgmtService.handleEvent({ + detail: { + id: "signIn", + data: { + method: "signIn", + email: clientEmail, + password: "123456", + }, + }, + }); +}); + +add_test(function testHandleGetAssertionError_defaultCase() { + do_test_pending(); + + // FxA device registration throws from this context + Services.prefs.setBoolPref("identity.fxaccounts.skipDeviceRegistration", true); + + FxAccountsManager.getAssertion(null).then( + success => { + // getAssertion should throw with invalid audience + ok(false); + }, + reason => { + equal("INVALID_AUDIENCE", reason.error); + do_test_finished(); + run_next_test(); + } + ) +}); + +// End of tests +// Utility functions follow + +function httpd_setup (handlers, port=-1) { + let server = new HttpServer(); + for (let path in handlers) { + server.registerPathHandler(path, handlers[path]); + } + try { + server.start(port); + } catch (ex) { + dump("ERROR starting server on port " + port + ". Already a process listening?"); + do_throw(ex); + } + + // Set the base URI for convenience. + let i = server.identity; + server.baseURI = i.primaryScheme + "://" + i.primaryHost + ":" + i.primaryPort; + + return server; +} + + diff --git a/b2g/components/test/unit/test_logcapture.js b/b2g/components/test/unit/test_logcapture.js new file mode 100644 index 000000000..9dbe2bc77 --- /dev/null +++ b/b2g/components/test/unit/test_logcapture.js @@ -0,0 +1,13 @@ +/** + * Testing non Gonk-specific code path + */ +function run_test() { + Components.utils.import("resource:///modules/LogCapture.jsm"); + run_next_test(); +} + +// Trivial test just to make sure we have no syntax error +add_test(function test_logCapture_loads() { + ok(LogCapture, "LogCapture object exists"); + run_next_test(); +}); diff --git a/b2g/components/test/unit/test_logcapture_gonk.js b/b2g/components/test/unit/test_logcapture_gonk.js new file mode 100644 index 000000000..d80f33dd9 --- /dev/null +++ b/b2g/components/test/unit/test_logcapture_gonk.js @@ -0,0 +1,70 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + + +/** + * Test that LogCapture successfully reads from the /dev/log devices, returning + * a Uint8Array of some length, including zero. This tests a few standard + * log devices + */ +function run_test() { + Components.utils.import("resource:///modules/LogCapture.jsm"); + run_next_test(); +} + +function verifyLog(log) { + // log exists + notEqual(log, null); + // log has a length and it is non-negative (is probably array-like) + ok(log.length >= 0); +} + +add_test(function test_readLogFile() { + let mainLog = LogCapture.readLogFile("/dev/log/main"); + verifyLog(mainLog); + + let meminfoLog = LogCapture.readLogFile("/proc/meminfo"); + verifyLog(meminfoLog); + + run_next_test(); +}); + +add_test(function test_readProperties() { + let propertiesLog = LogCapture.readProperties(); + notEqual(propertiesLog, null, "Properties should not be null"); + notEqual(propertiesLog, undefined, "Properties should not be undefined"); + + for (let propertyName in propertiesLog) { + equal(typeof(propertiesLog[propertyName]), "string", + "Property " + propertyName + " should be a string"); + } + + equal(propertiesLog["ro.product.locale.language"], "en", + "Locale language should be read correctly. See bug 1171577."); + + equal(propertiesLog["ro.product.locale.region"], "US", + "Locale region should be read correctly. See bug 1171577."); + + run_next_test(); +}); + +add_test(function test_readAppIni() { + let appIni = LogCapture.readLogFile("/system/b2g/application.ini"); + verifyLog(appIni); + + run_next_test(); +}); + +add_test(function test_get_about_memory() { + let memLog = LogCapture.readAboutMemory(); + + ok(memLog, "Should have returned a valid Promise object"); + + memLog.then(file => { + ok(file, "Should have returned a filename"); + run_next_test(); + }, error => { + ok(false, "Dumping about:memory promise rejected: " + error); + run_next_test(); + }); +}); diff --git a/b2g/components/test/unit/test_logparser.js b/b2g/components/test/unit/test_logparser.js new file mode 100644 index 000000000..624dcc6e2 --- /dev/null +++ b/b2g/components/test/unit/test_logparser.js @@ -0,0 +1,75 @@ +/* jshint moz: true */ + +var {utils: Cu, classes: Cc, interfaces: Ci} = Components; + +function debug(msg) { + var timestamp = Date.now(); + dump("LogParser: " + timestamp + ": " + msg + "\n"); +} + +function run_test() { + Cu.import("resource:///modules/LogParser.jsm"); + debug("Starting"); + run_next_test(); +} + +function makeStream(file) { + var fileStream = Cc["@mozilla.org/network/file-input-stream;1"] + .createInstance(Ci.nsIFileInputStream); + fileStream.init(file, -1, -1, 0); + var bis = Cc["@mozilla.org/binaryinputstream;1"] + .createInstance(Ci.nsIBinaryInputStream); + bis.setInputStream(fileStream); + return bis; +} + +add_test(function test_parse_logfile() { + let loggerFile = do_get_file("data/test_logger_file"); + + let loggerStream = makeStream(loggerFile); + + // Initialize arrays to hold the file contents (lengths are hardcoded) + let loggerArray = new Uint8Array(loggerStream.readByteArray(4037)); + + loggerStream.close(); + + let logMessages = LogParser.parseLogArray(loggerArray); + + ok(logMessages.length === 58, "There should be 58 messages in the log"); + + let expectedLogEntry = { + processId: 271, threadId: 271, + seconds: 790796, nanoseconds: 620000001, time: 790796620.000001, + priority: 4, tag: "Vold", + message: "Vold 2.1 (the revenge) firing up\n" + }; + + deepEqual(expectedLogEntry, logMessages[0]); + run_next_test(); +}); + +add_test(function test_print_properties() { + let properties = { + "ro.secure": "1", + "sys.usb.state": "diag,serial_smd,serial_tty,rmnet_bam,mass_storage,adb" + }; + + let logMessagesRaw = LogParser.prettyPrintPropertiesArray(properties); + let logMessages = new TextDecoder("utf-8").decode(logMessagesRaw); + let logMessagesArray = logMessages.split("\n"); + + ok(logMessagesArray.length === 3, "There should be 3 lines in the log."); + notEqual(logMessagesArray[0], "", "First line should not be empty"); + notEqual(logMessagesArray[1], "", "Second line should not be empty"); + equal(logMessagesArray[2], "", "Last line should be empty"); + + let expectedLog = [ + "[ro.secure]: [1]", + "[sys.usb.state]: [diag,serial_smd,serial_tty,rmnet_bam,mass_storage,adb]", + "" + ].join("\n"); + + deepEqual(expectedLog, logMessages); + + run_next_test(); +}); diff --git a/b2g/components/test/unit/test_logshake.js b/b2g/components/test/unit/test_logshake.js new file mode 100644 index 000000000..cfb81b893 --- /dev/null +++ b/b2g/components/test/unit/test_logshake.js @@ -0,0 +1,218 @@ +/** + * Test the log capturing capabilities of LogShake.jsm + */ + +/* jshint moz: true */ +/* global Components, LogCapture, LogShake, ok, add_test, run_next_test, dump */ +/* exported run_test */ + +/* disable use strict warning */ +/* jshint -W097 */ +"use strict"; + +var Cu = Components.utils; + +Cu.import("resource://gre/modules/LogCapture.jsm"); +Cu.import("resource://gre/modules/LogShake.jsm"); + +const EVENTS_PER_SECOND = 6.25; +const GRAVITY = 9.8; + +/** + * Force logshake to handle a device motion event with given components. + * Does not use SystemAppProxy because event needs special + * accelerationIncludingGravity property. + */ +function sendDeviceMotionEvent(x, y, z) { + let event = { + type: "devicemotion", + accelerationIncludingGravity: { + x: x, + y: y, + z: z + } + }; + LogShake.handleEvent(event); +} + +/** + * Send a screen change event directly, does not use SystemAppProxy due to race + * conditions. + */ +function sendScreenChangeEvent(screenEnabled) { + let event = { + type: "screenchange", + detail: { + screenEnabled: screenEnabled + } + }; + LogShake.handleEvent(event); +} + +/** + * Mock the readLogFile function of LogCapture. + * Used to detect whether LogShake activates. + * @return {Array<String>} Locations that LogShake tries to read + */ +function mockReadLogFile() { + let readLocations = []; + + LogCapture.readLogFile = function(loc) { + readLocations.push(loc); + return null; // we don't want to provide invalid data to a parser + }; + + // Allow inspection of readLocations by caller + return readLocations; +} + +/** + * Send a series of events that corresponds to a shake + */ +function sendSustainedShake() { + // Fire a series of devicemotion events that are of shake magnitude + for (let i = 0; i < 2 * EVENTS_PER_SECOND; i++) { + sendDeviceMotionEvent(0, 2 * GRAVITY, 2 * GRAVITY); + } + +} + +add_test(function test_do_log_capture_after_shaking() { + // Enable LogShake + LogShake.init(); + + let readLocations = mockReadLogFile(); + + sendSustainedShake(); + + ok(readLocations.length > 0, + "LogShake should attempt to read at least one log"); + + LogShake.uninit(); + run_next_test(); +}); + +add_test(function test_do_nothing_when_resting() { + // Enable LogShake + LogShake.init(); + + let readLocations = mockReadLogFile(); + + // Fire several devicemotion events that are relatively tiny + for (let i = 0; i < 2 * EVENTS_PER_SECOND; i++) { + sendDeviceMotionEvent(0, GRAVITY, GRAVITY); + } + + ok(readLocations.length === 0, + "LogShake should not read any logs"); + + LogShake.uninit(); + run_next_test(); +}); + +add_test(function test_do_nothing_when_disabled() { + // Disable LogShake + LogShake.uninit(); + + let readLocations = mockReadLogFile(); + + // Fire a series of events that would normally be a shake + sendSustainedShake(); + + ok(readLocations.length === 0, + "LogShake should not read any logs"); + + run_next_test(); +}); + +add_test(function test_do_nothing_when_screen_off() { + // Enable LogShake + LogShake.init(); + + // Send an event as if the screen has been turned off + sendScreenChangeEvent(false); + + let readLocations = mockReadLogFile(); + + // Fire a series of events that would normally be a shake + sendSustainedShake(); + + ok(readLocations.length === 0, + "LogShake should not read any logs"); + + // Restore the screen + sendScreenChangeEvent(true); + + LogShake.uninit(); + run_next_test(); +}); + +add_test(function test_do_log_capture_resilient_readLogFile() { + // Enable LogShake + LogShake.init(); + + let readLocations = []; + LogCapture.readLogFile = function(loc) { + readLocations.push(loc); + throw new Error("Exception during readLogFile for: " + loc); + }; + + // Fire a series of events that would normally be a shake + sendSustainedShake(); + + ok(readLocations.length > 0, + "LogShake should attempt to read at least one log"); + + LogShake.uninit(); + run_next_test(); +}); + +add_test(function test_do_log_capture_resilient_parseLog() { + // Enable LogShake + LogShake.init(); + + let readLocations = []; + LogCapture.readLogFile = function(loc) { + readLocations.push(loc); + LogShake.LOGS_WITH_PARSERS[loc] = function() { + throw new Error("Exception during LogParser for: " + loc); + }; + return null; + }; + + // Fire a series of events that would normally be a shake + sendSustainedShake(); + + ok(readLocations.length > 0, + "LogShake should attempt to read at least one log"); + + LogShake.uninit(); + run_next_test(); +}); + +add_test(function test_do_nothing_when_dropped() { + // Enable LogShake + LogShake.init(); + + let readLocations = mockReadLogFile(); + + // We want a series of spikes to be ignored by LogShake. This roughly + // corresponds to the compare_stairs_sock graph on bug #1101994 + + for (let i = 0; i < 10 * EVENTS_PER_SECOND; i++) { + // Fire a devicemotion event that is at rest + sendDeviceMotionEvent(0, 0, GRAVITY); + // Fire a spike of motion + sendDeviceMotionEvent(0, 2 * GRAVITY, 2 * GRAVITY); + } + + ok(readLocations.length === 0, + "LogShake should not read any logs"); + + LogShake.uninit(); + run_next_test(); +}); + +function run_test() { + run_next_test(); +} diff --git a/b2g/components/test/unit/test_logshake_gonk.js b/b2g/components/test/unit/test_logshake_gonk.js new file mode 100644 index 000000000..28de0263f --- /dev/null +++ b/b2g/components/test/unit/test_logshake_gonk.js @@ -0,0 +1,61 @@ +/** + * Test the log capturing capabilities of LogShake.jsm, checking + * for Gonk-specific parts + */ + +/* jshint moz: true, esnext: true */ +/* global Cu, LogCapture, LogShake, ok, add_test, run_next_test, dump, + setup_logshake_mocks, OS, sdcard */ +/* exported run_test */ + +/* disable use strict warning */ +/* jshint -W097 */ + +"use strict"; + +Cu.import("resource://gre/modules/Promise.jsm"); + +function run_test() { + Cu.import("resource://gre/modules/LogShake.jsm"); + run_next_test(); +} + +add_test(setup_logshake_mocks); + +add_test(function test_logShake_captureLogs_writes() { + // Enable LogShake + LogShake.init(); + + let expectedFiles = []; + + LogShake.captureLogs().then(logResults => { + LogShake.uninit(); + + ok(logResults.logFilenames.length > 0, "Should have filenames"); + ok(logResults.logPaths.length > 0, "Should have paths"); + ok(!logResults.compressed, "Should not be compressed"); + + logResults.logPaths.forEach(f => { + let p = OS.Path.join(sdcard, f); + ok(p, "Should have a valid result path: " + p); + + let t = OS.File.exists(p).then(rv => { + ok(rv, "File exists: " + p); + }); + + expectedFiles.push(t); + }); + + Promise.all(expectedFiles).then(() => { + ok(true, "Completed all files checks"); + run_next_test(); + }); + }, + error => { + LogShake.uninit(); + + ok(false, "Should not have received error: " + error); + + run_next_test(); + }); +}); diff --git a/b2g/components/test/unit/test_logshake_gonk_compression.js b/b2g/components/test/unit/test_logshake_gonk_compression.js new file mode 100644 index 000000000..b5af46081 --- /dev/null +++ b/b2g/components/test/unit/test_logshake_gonk_compression.js @@ -0,0 +1,76 @@ +/** + * Test the log capturing capabilities of LogShake.jsm, checking + * for Gonk-specific parts + */ + +/* jshint moz: true, esnext: true */ +/* global Cc, Ci, Cu, LogCapture, LogShake, ok, add_test, run_next_test, dump, + setup_logshake_mocks, OS, sdcard, FileUtils */ +/* exported run_test */ + +/* disable use strict warning */ +/* jshint -W097 */ + +"use strict"; + +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/FileUtils.jsm"); + +function run_test() { + Cu.import("resource://gre/modules/LogShake.jsm"); + run_next_test(); +} + +add_test(setup_logshake_mocks); + +add_test(function test_logShake_captureLogs_writes_zip() { + // Enable LogShake + LogShake.init(); + + let expectedFiles = []; + + LogShake.enableQAMode(); + + LogShake.captureLogs().then(logResults => { + LogShake.uninit(); + + ok(logResults.logPaths.length === 1, "Should have zip path"); + ok(logResults.logFilenames.length >= 1, "Should have log filenames"); + ok(logResults.compressed, "Log files should be compressed"); + + let zipPath = OS.Path.join(sdcard, logResults.logPaths[0]); + ok(zipPath, "Should have a valid archive path: " + zipPath); + + let zipFile = new FileUtils.File(zipPath); + ok(zipFile, "Should have a valid archive file: " + zipFile); + + let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"] + .createInstance(Ci.nsIZipReader); + zipReader.open(zipFile); + + let logFilenamesSeen = {}; + + let zipEntries = zipReader.findEntries(null); // Find all entries + while (zipEntries.hasMore()) { + let entryName = zipEntries.getNext(); + let entry = zipReader.getEntry(entryName); + logFilenamesSeen[entryName] = true; + ok(!entry.isDirectory, "Archive entry " + entryName + " should be a file"); + } + zipReader.close(); + + // TODO: Verify archive contents + logResults.logFilenames.forEach(filename => { + ok(logFilenamesSeen[filename], "File " + filename + " should be present in archive"); + }); + run_next_test(); + }, + error => { + LogShake.uninit(); + + ok(false, "Should not have received error: " + error); + + run_next_test(); + }); +}); + diff --git a/b2g/components/test/unit/test_logshake_readLog_gonk.js b/b2g/components/test/unit/test_logshake_readLog_gonk.js new file mode 100644 index 000000000..003723ad5 --- /dev/null +++ b/b2g/components/test/unit/test_logshake_readLog_gonk.js @@ -0,0 +1,65 @@ +/** + * Test the log capturing capabilities of LogShake.jsm under conditions that + * could cause races + */ + +/* jshint moz: true, esnext: true */ +/* global Cu, LogCapture, LogShake, ok, add_test, run_next_test, dump, + XPCOMUtils, do_get_profile, OS, volumeService, Promise, equal, + setup_logshake_mocks */ +/* exported run_test */ + +/* disable use strict warning */ +/* jshint -W097 */ + +"use strict"; + +function run_test() { + Cu.import("resource://gre/modules/LogShake.jsm"); + run_next_test(); +} + +add_test(setup_logshake_mocks); + +add_test(function test_logShake_captureLogs_waits_to_read() { + // Enable LogShake + LogShake.init(); + + // Save no logs synchronously (except properties) + LogShake.LOGS_WITH_PARSERS = {}; + + LogShake.captureLogs().then(logResults => { + LogShake.uninit(); + + ok(logResults.logFilenames.length > 0, "Should have filenames"); + ok(logResults.logPaths.length > 0, "Should have paths"); + ok(!logResults.compressed, "Should not be compressed"); + + // This assumes that the about:memory reading will only fail under abnormal + // circumstances. It does not check for screenshot.png because + // systemAppFrame is unavailable during xpcshell tests. + let hasAboutMemory = false; + + logResults.logFilenames.forEach(filename => { + // Because the about:memory log's filename has the PID in it we can not + // use simple equality but instead search for the "about_memory" part of + // the filename which will look like logshake-about_memory-{PID}.json.gz + if (filename.indexOf("about_memory") < 0) { + return; + } + hasAboutMemory = true; + }); + + ok(hasAboutMemory, + "LogShake's asynchronous read of about:memory should have succeeded."); + + run_next_test(); + }, + error => { + LogShake.uninit(); + + ok(false, "Should not have received error: " + error); + + run_next_test(); + }); +}); diff --git a/b2g/components/test/unit/test_signintowebsite.js b/b2g/components/test/unit/test_signintowebsite.js new file mode 100644 index 000000000..38d4fa79e --- /dev/null +++ b/b2g/components/test/unit/test_signintowebsite.js @@ -0,0 +1,322 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests for b2g/components/SignInToWebsite.jsm + +"use strict"; + +XPCOMUtils.defineLazyModuleGetter(this, "MinimalIDService", + "resource://gre/modules/identity/MinimalIdentity.jsm", + "IdentityService"); + +XPCOMUtils.defineLazyModuleGetter(this, "SignInToWebsiteController", + "resource://gre/modules/SignInToWebsite.jsm", + "SignInToWebsiteController"); + +Cu.import("resource://gre/modules/identity/LogUtils.jsm"); + +function log(...aMessageArgs) { + Logger.log.apply(Logger, ["test_signintowebsite"].concat(aMessageArgs)); +} + +function test_overall() { + do_check_neq(MinimalIDService, null); + run_next_test(); +} + +function objectContains(object, subset) { + let objectKeys = Object.keys(object); + let subsetKeys = Object.keys(subset); + + // can't have fewer keys than the subset + if (objectKeys.length < subsetKeys.length) { + return false; + } + + let key; + let success = true; + if (subsetKeys.length > 0) { + for (let i=0; i<subsetKeys.length; i++) { + key = subsetKeys[i]; + + // key exists in the source object + if (typeof object[key] === 'undefined') { + success = false; + break; + } + + // recursively check object values + else if (typeof subset[key] === 'object') { + if (typeof object[key] !== 'object') { + success = false; + break; + } + if (! objectContains(object[key], subset[key])) { + success = false; + break; + } + } + + else if (object[key] !== subset[key]) { + success = false; + break; + } + } + } + + return success; +} + +function test_object_contains() { + do_test_pending(); + + let someObj = { + pies: 42, + green: "spam", + flan: {yes: "please"} + }; + let otherObj = { + pies: 42, + flan: {yes: "please"} + }; + do_check_true(objectContains(someObj, otherObj)); + do_test_finished(); + run_next_test(); +} + +function test_mock_doc() { + do_test_pending(); + let mockedDoc = mockDoc({loggedInUser: null}, function(action, params) { + do_check_eq(action, 'coffee'); + do_test_finished(); + run_next_test(); + }); + + // A smoke test to ensure that mockedDoc is functioning correctly. + // There is presently no doCoffee method in Persona. + mockedDoc.doCoffee(); +} + +function test_watch() { + do_test_pending(); + + setup_test_identity("pie@food.gov", TEST_CERT, function() { + let controller = SignInToWebsiteController; + + let mockedDoc = mockDoc({loggedInUser: null}, function(action, params) { + do_check_eq(action, 'ready'); + controller.uninit(); + MinimalIDService.RP.unwatch(mockedDoc.id); + do_test_finished(); + run_next_test(); + }); + + controller.init({pipe: mockReceivingPipe()}); + MinimalIDService.RP.watch(mockedDoc, {}); + }); +} + +function test_request_login() { + do_test_pending(); + + setup_test_identity("flan@food.gov", TEST_CERT, function() { + let controller = SignInToWebsiteController; + + let mockedDoc = mockDoc({loggedInUser: null}, call_sequentially( + function(action, params) { + do_check_eq(action, 'ready'); + do_check_eq(params, undefined); + }, + function(action, params) { + do_check_eq(action, 'login'); + do_check_eq(params, TEST_CERT); + controller.uninit(); + MinimalIDService.RP.unwatch(mockedDoc.id); + do_test_finished(); + run_next_test(); + } + )); + + controller.init({pipe: mockReceivingPipe()}); + MinimalIDService.RP.watch(mockedDoc, {}); + MinimalIDService.RP.request(mockedDoc.id, {}); + }); +} + +function test_request_logout() { + do_test_pending(); + + setup_test_identity("flan@food.gov", TEST_CERT, function() { + let controller = SignInToWebsiteController; + + let mockedDoc = mockDoc({loggedInUser: null}, call_sequentially( + function(action, params) { + do_check_eq(action, 'ready'); + do_check_eq(params, undefined); + }, + function(action, params) { + do_check_eq(action, 'logout'); + do_check_eq(params, undefined); + controller.uninit(); + MinimalIDService.RP.unwatch(mockedDoc.id); + do_test_finished(); + run_next_test(); + } + )); + + controller.init({pipe: mockReceivingPipe()}); + MinimalIDService.RP.watch(mockedDoc, {}); + MinimalIDService.RP.logout(mockedDoc.id, {}); + }); +} + +function test_request_login_logout() { + do_test_pending(); + + setup_test_identity("unagi@food.gov", TEST_CERT, function() { + let controller = SignInToWebsiteController; + + let mockedDoc = mockDoc({loggedInUser: null}, call_sequentially( + function(action, params) { + do_check_eq(action, 'ready'); + do_check_eq(params, undefined); + }, + function(action, params) { + do_check_eq(action, 'login'); + do_check_eq(params, TEST_CERT); + }, + function(action, params) { + do_check_eq(action, 'logout'); + do_check_eq(params, undefined); + controller.uninit(); + MinimalIDService.RP.unwatch(mockedDoc.id); + do_test_finished(); + run_next_test(); + } + )); + + controller.init({pipe: mockReceivingPipe()}); + MinimalIDService.RP.watch(mockedDoc, {}); + MinimalIDService.RP.request(mockedDoc.id, {}); + MinimalIDService.RP.logout(mockedDoc.id, {}); + }); +} + +function test_logout_everywhere() { + do_test_pending(); + let logouts = 0; + + setup_test_identity("fugu@food.gov", TEST_CERT, function() { + let controller = SignInToWebsiteController; + + let mockedDoc1 = mockDoc({loggedInUser: null}, call_sequentially( + function(action, params) { + do_check_eq(action, 'ready'); + }, + function(action, params) { + do_check_eq(action, 'login'); + }, + function(action, params) { + // Result of logout from doc2. + // We don't know what order the logouts will occur in. + do_check_eq(action, 'logout'); + if (++logouts === 2) { + do_test_finished(); + run_next_test(); + } + } + )); + + let mockedDoc2 = mockDoc({loggedInUser: null}, call_sequentially( + function(action, params) { + do_check_eq(action, 'ready'); + }, + function(action, params) { + do_check_eq(action, 'login'); + }, + function(action, params) { + do_check_eq(action, 'logout'); + if (++logouts === 2) { + do_test_finished(); + run_next_test(); + } + } + )); + + controller.init({pipe: mockReceivingPipe()}); + MinimalIDService.RP.watch(mockedDoc1, {}); + MinimalIDService.RP.request(mockedDoc1.id, {}); + + MinimalIDService.RP.watch(mockedDoc2, {}); + MinimalIDService.RP.request(mockedDoc2.id, {}); + + // Logs out of both docs because they share the + // same origin. + MinimalIDService.RP.logout(mockedDoc2.id, {}); + }); +} + +function test_options_pass_through() { + do_test_pending(); + + // An meaningless structure for testing that RP messages preserve + // objects and their parameters as they are passed back and forth. + let randomMixedParams = { + loggedInUser: "juanita@mozilla.com", + forceAuthentication: true, + forceIssuer: "foo.com", + someThing: { + name: "Pertelote", + legs: 4, + nested: {bee: "Eric", remaining: "1/2"} + } + }; + + let mockedDoc = mockDoc(randomMixedParams, function(action, params) {}); + + function pipeOtherEnd(rpOptions, gaiaOptions) { + // Ensure that every time we receive a message, our mixed + // random params are contained in that message + do_check_true(objectContains(rpOptions, randomMixedParams)); + + switch (gaiaOptions.message) { + case "identity-delegate-watch": + MinimalIDService.RP.request(mockedDoc.id, {}); + break; + case "identity-delegate-request": + MinimalIDService.RP.logout(mockedDoc.id, {}); + break; + case "identity-delegate-logout": + do_test_finished(); + controller.uninit(); + MinimalIDService.RP.unwatch(mockedDoc.id); + run_next_test(); + break; + } + } + + let controller = SignInToWebsiteController; + controller.init({pipe: mockSendingPipe(pipeOtherEnd)}); + + MinimalIDService.RP.watch(mockedDoc, {}); +} + +var TESTS = [ + test_overall, + test_mock_doc, + test_object_contains, + + test_watch, + test_request_login, + test_request_logout, + test_request_login_logout, + test_logout_everywhere, + + test_options_pass_through +]; + +TESTS.forEach(add_test); + +function run_test() { + run_next_test(); +} diff --git a/b2g/components/test/unit/xpcshell.ini b/b2g/components/test/unit/xpcshell.ini new file mode 100644 index 000000000..ca3df5bf6 --- /dev/null +++ b/b2g/components/test/unit/xpcshell.ini @@ -0,0 +1,49 @@ +[DEFAULT] +head = +tail = + +support-files = + data/test_logger_file + +[test_bug793310.js] + +[test_bug832946.js] + +[test_fxaccounts.js] +[test_signintowebsite.js] +head = head_identity.js +tail = + +# testing non gonk-specific stuff +[test_logcapture.js] + +[test_logcapture_gonk.js] +# can be slow because of what the test does, so let's give it some more time +# to avoid intermittents: bug 1212395 +requesttimeoutfactor = 2 +# only run on b2g builds due to requiring b2g-specific log files to exist +skip-if = toolkit != "gonk" + +[test_logparser.js] + +[test_logshake.js] + +[test_logshake_gonk.js] +# can be slow because of what the test does, so let's give it some more time +# to avoid intermittents: bug 1144499 +requesttimeoutfactor = 2 +head = head_logshake_gonk.js +# only run on b2g builds due to requiring b2g-specific log files to exist +skip-if = (toolkit != "gonk") + +[test_logshake_gonk_compression.js] +head = head_logshake_gonk.js +# only run on b2g builds due to requiring b2g-specific log files to exist +skip-if = (toolkit != "gonk") + +[test_logshake_readLog_gonk.js] +head = head_logshake_gonk.js +# only run on b2g builds due to requiring b2g-specific log files to exist +skip-if = (toolkit != "gonk") + +[test_aboutserviceworkers.js] |