Merge m-c to b2ginbound, a=merge

This commit is contained in:
Wes Kocher 2015-09-25 18:03:13 -07:00
Родитель dfb8ba7f2b 15a2e0fa73
Коммит bf91aa6821
35 изменённых файлов: 555 добавлений и 554 удалений

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

@ -399,7 +399,7 @@ var FullScreen = {
Cu.import("resource://gre/modules/DownloadUtils.jsm", utils);
hostLabel.value = utils.DownloadUtils.getURIHost(uri.spec)[0];
}
this._element.className = gIdentityHandler.getMode();
this._element.className = gIdentityHandler.fullscreenWarningClassName;
// User should be allowed to explicitly disable
// the prompt if they really want.

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

@ -4506,33 +4506,6 @@ var XULBrowserWindow = {
throw "onSecurityChange: aIsSimulated receieved an unexpected type";
}
// aState is defined as a bitmask that may be extended in the future.
// We filter out any unknown bits before testing for known values.
const wpl = Components.interfaces.nsIWebProgressListener;
const wpl_security_bits = wpl.STATE_IS_SECURE |
wpl.STATE_IS_BROKEN |
wpl.STATE_IS_INSECURE;
var level;
switch (this._state & wpl_security_bits) {
case wpl.STATE_IS_SECURE:
level = "high";
break;
case wpl.STATE_IS_BROKEN:
level = "broken";
break;
}
if (level) {
// We don't style the Location Bar based on the the 'level' attribute
// anymore, but still set it for third-party themes.
if (gURLBar)
gURLBar.setAttribute("level", level);
} else {
if (gURLBar)
gURLBar.removeAttribute("level");
}
// Make sure the "https" part of the URL is striked out or not,
// depending on the current mixed active content blocking state.
gURLBar.formatValue();
@ -4540,7 +4513,7 @@ var XULBrowserWindow = {
try {
uri = Services.uriFixup.createExposableURI(uri);
} catch (e) {}
gIdentityHandler.checkIdentity(this._state, uri);
gIdentityHandler.updateIdentity(this._state, uri);
TrackingProtection.onSecurityChange(this._state, aIsSimulated);
},
@ -6741,22 +6714,64 @@ function formatURL(aFormat, aIsPref) {
* Utility object to handle manipulations of the identity indicators in the UI
*/
var gIdentityHandler = {
// Mode strings used to control CSS display
IDENTITY_MODE_IDENTIFIED : "verifiedIdentity", // High-quality identity information
IDENTITY_MODE_DOMAIN_VERIFIED : "verifiedDomain", // Minimal SSL CA-signed domain verification
IDENTITY_MODE_UNKNOWN : "unknownIdentity", // No trusted identity information
IDENTITY_MODE_USES_WEAK_CIPHER : "unknownIdentity weakCipher", // SSL with RC4 cipher suite or SSL3
IDENTITY_MODE_MIXED_DISPLAY_LOADED : "unknownIdentity mixedContent mixedDisplayContent", // SSL with unauthenticated display content
IDENTITY_MODE_MIXED_ACTIVE_LOADED : "unknownIdentity mixedContent mixedActiveContent", // SSL with unauthenticated active (and perhaps also display) content
IDENTITY_MODE_MIXED_DISPLAY_LOADED_ACTIVE_BLOCKED : "unknownIdentity mixedContent mixedDisplayContentLoadedActiveBlocked", // SSL with unauthenticated display content; unauthenticated active content is blocked.
IDENTITY_MODE_MIXED_ACTIVE_BLOCKED : "verifiedDomain mixedContent mixedActiveBlocked", // SSL with unauthenticated active content blocked; no unauthenticated display content
IDENTITY_MODE_MIXED_ACTIVE_BLOCKED_IDENTIFIED : "verifiedIdentity mixedContent mixedActiveBlocked", // SSL with unauthenticated active content blocked; no unauthenticated display content
IDENTITY_MODE_CHROMEUI : "chromeUI", // Part of the product's UI
_isChromeUI: false,
_sslStatus: null,
/**
* nsIURI for which the identity UI is displayed. This has been already
* processed by nsIURIFixup.createExposableURI.
*/
_uri: null,
/**
* We only know the connection type if this._uri has a defined "host" part.
*
* These URIs, like "about:" and "data:" URIs, will usually be treated as a
* non-secure connection, unless they refer to an internally implemented
* browser page or resolve to "file:" URIs.
*/
_uriHasHost: false,
/**
* Whether this._uri refers to an internally implemented browser page.
*
* Note that this is set for some "about:" pages, but general "chrome:" URIs
* are not included in this category by default.
*/
_isSecureInternalUI: false,
/**
* nsISSLStatus metadata provided by gBrowser.securityUI the last time the
* identity UI was updated, or null if the connection is not secure.
*/
_sslStatus: null,
/**
* Bitmask provided by nsIWebProgressListener.onSecurityChange.
*/
_state: 0,
get _isBroken() {
return this._state & Ci.nsIWebProgressListener.STATE_IS_BROKEN;
},
get _isSecure() {
return this._state & Ci.nsIWebProgressListener.STATE_IS_SECURE;
},
get _isEV() {
return this._state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL;
},
get _isMixedActiveContentLoaded() {
return this._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT;
},
get _isMixedActiveContentBlocked() {
return this._state & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT;
},
get _isMixedPassiveContentLoaded() {
return this._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT;
},
// smart getters
get _identityPopup () {
delete this._identityPopup;
@ -6888,8 +6903,7 @@ var gIdentityHandler = {
*/
getIdentityData : function() {
var result = {};
var status = this._sslStatus.QueryInterface(Ci.nsISSLStatus);
var cert = status.serverCert;
var cert = this._sslStatus.serverCert;
// Human readable name of Subject
result.subjectOrg = cert.organization;
@ -6916,69 +6930,45 @@ var gIdentityHandler = {
},
/**
* Determine the identity of the page being displayed by examining its SSL cert
* (if available) and, if necessary, update the UI to reflect this. Intended to
* be called by onSecurityChange
* Update the identity user interface for the page currently being displayed.
*
* @param PRUint32 state
* @param nsIURI uri The address for which the UI should be updated.
* This examines the SSL certificate metadata, if available, as well as the
* connection type and other security-related state information for the page.
*
* @param state
* Bitmask provided by nsIWebProgressListener.onSecurityChange.
* @param uri
* nsIURI for which the identity UI should be displayed, already
* processed by nsIURIFixup.createExposableURI.
*/
checkIdentity : function(state, uri) {
let nsIWebProgressListener = Ci.nsIWebProgressListener;
updateIdentity(state, uri) {
this._state = state;
this._uri = uri;
// Firstly, populate the state properties required to display the UI. See
// the documentation of the individual properties for details.
// For some URIs like data: we can't get a host. URIs without a host will
// usually be treated as a non-secure connection if they're not on the
// whitelist below and don't resolve to file:// URIs internally.
let unknown = false;
try {
uri.host;
} catch (e) { unknown = true; }
// Chrome URIs however get special treatment. Some chrome URIs are
// whitelisted to provide a positive security signal to the user.
let whitelist = /^(?:accounts|addons|app-manager|cache|config|crashes|customizing|downloads|healthreport|home|license|newaddon|permissions|preferences|privatebrowsing|rights|sessionrestore|support|welcomeback)(?:[?#]|$)/i;
let isChromeUI = uri.schemeIs("about") && whitelist.test(uri.path);
let mode = this.IDENTITY_MODE_UNKNOWN;
if (isChromeUI) {
mode = this.IDENTITY_MODE_CHROMEUI;
} else if (unknown) {
// Use default mode.
} else if (state & nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) {
if (state & nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) {
mode = this.IDENTITY_MODE_MIXED_ACTIVE_BLOCKED_IDENTIFIED;
} else {
mode = this.IDENTITY_MODE_IDENTIFIED;
}
} else if (state & nsIWebProgressListener.STATE_IS_SECURE) {
if (state & nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) {
mode = this.IDENTITY_MODE_MIXED_ACTIVE_BLOCKED;
} else {
mode = this.IDENTITY_MODE_DOMAIN_VERIFIED;
}
} else if (state & nsIWebProgressListener.STATE_IS_BROKEN) {
if (state & nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) {
mode = this.IDENTITY_MODE_MIXED_ACTIVE_LOADED;
} else if (state & nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) {
mode = this.IDENTITY_MODE_MIXED_DISPLAY_LOADED_ACTIVE_BLOCKED;
} else if (state & nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT) {
mode = this.IDENTITY_MODE_MIXED_DISPLAY_LOADED;
} else {
mode = this.IDENTITY_MODE_USES_WEAK_CIPHER;
}
this._uri.host;
this._uriHasHost = true;
} catch (ex) {
this._uriHasHost = false;
}
// We need those values later when populating the control center.
this._uri = uri;
this._state = state;
this._isChromeUI = isChromeUI;
this._sslStatus =
gBrowser.securityUI.QueryInterface(Ci.nsISSLStatusProvider).SSLStatus;
let whitelist = /^(?:accounts|addons|app-manager|cache|config|crashes|customizing|downloads|healthreport|home|license|newaddon|permissions|preferences|privatebrowsing|rights|sessionrestore|support|welcomeback)(?:[?#]|$)/i;
this._isSecureInternalUI = uri.schemeIs("about") && whitelist.test(uri.path);
this._sslStatus = gBrowser.securityUI
.QueryInterface(Ci.nsISSLStatusProvider)
.SSLStatus;
if (this._sslStatus) {
this._sslStatus.QueryInterface(Ci.nsISSLStatus);
}
// Then, update the user interface with the available data.
// Update the identity block.
if (this._identityBox) {
this._identityBox.className = mode;
this.refreshIdentityBlock(mode);
this.refreshIdentityBlock();
}
// NOTE: We do NOT update the identity popup (the control center) when
@ -7004,48 +6994,40 @@ var gIdentityHandler = {
},
/**
* Return the current mode, which should be one of IDENTITY_MODE_*.
* Return the CSS class name to set on the "fullscreen-warning" element to
* display information about connection security in the notification shown
* when a site enters the fullscreen mode.
*/
getMode: function() {
return this._mode;
get fullscreenWarningClassName() {
// Note that the fullscreen warning does not handle _isSecureInternalUI.
if (this._uriHasHost && this._isEV) {
return "verifiedIdentity";
}
if (this._uriHasHost && this._isSecure) {
return "verifiedDomain";
}
return "unknownIdentity";
},
/**
* Set up the messages for the primary identity UI based on the specified mode,
* and the details of the SSL cert, where applicable
*
* @param newMode The newly set identity mode. Should be one of the IDENTITY_MODE_* constants.
* Updates the identity block user interface with the data from this object.
*/
refreshIdentityBlock(newMode) {
refreshIdentityBlock() {
let icon_label = "";
let tooltip = "";
let icon_country_label = "";
let icon_labels_dir = "ltr";
switch (newMode) {
case this.IDENTITY_MODE_DOMAIN_VERIFIED:
case this.IDENTITY_MODE_MIXED_ACTIVE_BLOCKED: {
let iData = this.getIdentityData();
if (this._isSecureInternalUI) {
this._identityBox.className = "chromeUI";
let brandBundle = document.getElementById("bundle_brand");
icon_label = brandBundle.getString("brandShorterName");
} else if (this._uriHasHost && this._isEV) {
this._identityBox.className = "verifiedIdentity";
if (this._isMixedActiveContentBlocked) {
this._identityBox.classList.add("mixedActiveBlocked");
}
// Verifier is either the CA Org, for a normal cert, or a special string
// for certs that are trusted because of a security exception.
tooltip = gNavigatorBundle.getFormattedString("identity.identified.verifier",
[iData.caOrg]);
// This can't throw, because URI's with a host that throw don't end up in this case.
let host = this._uri.host;
let port = 443;
try {
if (this._uri.port > 0)
port = this._uri.port;
} catch (e) {}
if (this._overrideService.hasMatchingOverride(host, port, iData.cert, {}, {}))
tooltip = gNavigatorBundle.getString("identity.identified.verified_by_you");
break; }
case this.IDENTITY_MODE_IDENTIFIED:
case this.IDENTITY_MODE_MIXED_ACTIVE_BLOCKED_IDENTIFIED: {
// If it's identified, then we can populate the dialog with credentials
let iData = this.getIdentityData();
tooltip = gNavigatorBundle.getFormattedString("identity.identified.verifier",
@ -7062,12 +7044,43 @@ var gIdentityHandler = {
// Unicode Bidirectional Algorithm proper (at the paragraph level).
icon_labels_dir = /^[\u0590-\u08ff\ufb1d-\ufdff\ufe70-\ufefc]/.test(icon_label) ?
"rtl" : "ltr";
break; }
case this.IDENTITY_MODE_CHROMEUI:
let brandBundle = document.getElementById("bundle_brand");
icon_label = brandBundle.getString("brandShorterName");
break;
default:
} else if (this._uriHasHost && this._isSecure) {
this._identityBox.className = "verifiedDomain";
if (this._isMixedActiveContentBlocked) {
this._identityBox.classList.add("mixedActiveBlocked");
}
let iData = this.getIdentityData();
// Verifier is either the CA Org, for a normal cert, or a special string
// for certs that are trusted because of a security exception.
tooltip = gNavigatorBundle.getFormattedString("identity.identified.verifier",
[iData.caOrg]);
let host = this._uri.host;
let port = 443;
try {
if (this._uri.port > 0)
port = this._uri.port;
} catch (e) {}
if (this._overrideService.hasMatchingOverride(host, port, iData.cert, {}, {})) {
tooltip = gNavigatorBundle.getString("identity.identified.verified_by_you");
}
} else {
this._identityBox.className = "unknownIdentity";
if (this._isBroken) {
if (this._isMixedActiveContentLoaded) {
this._identityBox.classList.add("mixedActiveContent");
} else if (this._isMixedActiveContentBlocked) {
this._identityBox.classList.add("mixedDisplayContentLoadedActiveBlocked");
} else if (this._isMixedPassiveContentLoaded) {
this._identityBox.classList.add("mixedDisplayContent");
} else {
this._identityBox.classList.add("weakCipher");
}
}
tooltip = gNavigatorBundle.getString("identity.unknown.tooltip");
}
@ -7093,39 +7106,26 @@ var gIdentityHandler = {
let learnMoreHref = `${baseURL}mixed-content`;
this._identityPopupMixedContentLearnMore.setAttribute("href", learnMoreHref);
// Basic connection properties.
let isBroken = this._state & Ci.nsIWebProgressListener.STATE_IS_BROKEN;
let isSecure = this._state & Ci.nsIWebProgressListener.STATE_IS_SECURE;
let isEV = this._state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL;
// Determine connection security information.
let connection = "not-secure";
if (this._isChromeUI) {
if (this._isSecureInternalUI) {
connection = "chrome";
} else if (this._isURILoadedFromFile(this._uri)) {
} else if (this._isURILoadedFromFile) {
connection = "file";
} else if (isEV) {
} else if (this._isEV) {
connection = "secure-ev";
} else if (isSecure) {
} else if (this._isSecure) {
connection = "secure";
}
// Mixed content flags.
let isMixedActiveContentLoaded =
this._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT;
let isMixedActiveContentBlocked =
this._state & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT;
let isMixedPassiveContentLoaded =
this._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT;
// Determine the mixed content state.
let mixedcontent = [];
if (isMixedPassiveContentLoaded) {
if (this._isMixedPassiveContentLoaded) {
mixedcontent.push("passive-loaded");
}
if (isMixedActiveContentLoaded) {
if (this._isMixedActiveContentLoaded) {
mixedcontent.push("active-loaded");
} else if (isMixedActiveContentBlocked) {
} else if (this._isMixedActiveContentBlocked) {
mixedcontent.push("active-blocked");
}
mixedcontent = mixedcontent.join(" ");
@ -7134,7 +7134,7 @@ var gIdentityHandler = {
// broken and we can't detect any mixed content loaded then it's a weak
// cipher.
let ciphers = "";
if (isBroken && !isMixedActiveContentLoaded && !isMixedPassiveContentLoaded) {
if (this._isBroken && !this._isMixedActiveContentLoaded && !this._isMixedPassiveContentLoaded) {
ciphers = "weak";
}
@ -7157,7 +7157,7 @@ var gIdentityHandler = {
updateAttribute(element, "connection", connection);
updateAttribute(element, "ciphers", ciphers);
updateAttribute(element, "mixedcontent", mixedcontent);
updateAttribute(element, "isbroken", isBroken);
updateAttribute(element, "isbroken", this._isBroken);
}
// Initialize the optional strings to empty values
@ -7179,12 +7179,12 @@ var gIdentityHandler = {
}
// Fill in the CA name if we have a valid TLS certificate.
if (isSecure) {
if (this._isSecure) {
verifier = this._identityBox.tooltipText;
}
// Fill in organization information if we have a valid EV certificate.
if (isEV) {
if (this._isEV) {
crop = "end";
let iData = this.getIdentityData();
@ -7216,10 +7216,10 @@ var gIdentityHandler = {
this.updateSitePermissions();
},
_isURILoadedFromFile(uri) {
get _isURILoadedFromFile() {
// Create a channel for the sole purpose of getting the resolved URI
// of the request to determine if it's loaded from the file system.
let chanOptions = {uri, loadUsingSystemPrincipal: true};
let chanOptions = {uri: this._uri, loadUsingSystemPrincipal: true};
let resolvedURI;
try {
resolvedURI = NetUtil.newChannel(chanOptions).URI;

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

@ -623,17 +623,12 @@ ContentSearchUIController.prototype = {
return;
}
let searchWithHeader = document.getElementById("contentSearchSearchWithHeader");
while (searchWithHeader.firstChild) {
searchWithHeader.firstChild.remove();
}
if (this.input.value) {
let html = "<span class='contentSearchSearchWithHeaderSearchText'>" +
this.input.value + "</span>";
html = this._strings.searchForKeywordsWith.replace("%S", html);
searchWithHeader.innerHTML = html;
return;
searchWithHeader.innerHTML = this._strings.searchForSomethingWith;
searchWithHeader.querySelector('.contentSearchSearchWithHeaderSearchText').textContent = this.input.value;
} else {
searchWithHeader.textContent = this._strings.searchWithHeader;
}
searchWithHeader.appendChild(document.createTextNode(this._strings.searchWithHeader));
},
_speculativeConnect: function () {

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

@ -21,8 +21,7 @@ add_task(function* checkIdentityOfAboutSupport() {
yield promiseTabLoaded(tab);
let identityBox = document.getElementById("identity-box");
is(identityBox.className, gIdentityHandler.IDENTITY_MODE_CHROMEUI,
"Should know that we're chrome.");
is(identityBox.className, "chromeUI", "Should know that we're chrome.");
gBrowser.removeTab(tab);
});

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

@ -5,7 +5,7 @@ function test() {
gBrowser.selectedBrowser.addEventListener("load", function () {
gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
is(document.getElementById("identity-box").className,
gIdentityHandler.IDENTITY_MODE_MIXED_DISPLAY_LOADED,
"unknownIdentity mixedDisplayContent",
"identity box has class name for mixed content");
gBrowser.removeCurrentTab();

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

@ -107,7 +107,8 @@ function checkResult() {
// getEffectiveHost can't be called for all modes
if (gCurrentTest.effectiveHost === null) {
let identityBox = document.getElementById("identity-box");
is(identityBox.className == gIdentityHandler.IDENTITY_MODE_UNKNOWN || identityBox.className == gIdentityHandler.IDENTITY_MODE_CHROMEUI, true, "mode matched");
ok(identityBox.className == "unknownIdentity" ||
identityBox.className == "chromeUI", "mode matched");
} else {
is(gIdentityHandler.getEffectiveHost(), gCurrentTest.effectiveHost, "effectiveHost matches for test " + gTestDesc);
}

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

@ -806,7 +806,6 @@ function assertMixedContentBlockingState(tabbrowser, states = {}) {
ok(!classList.contains("mixedActiveBlocked"), "No MCB icon on HTTP page");
ok(!classList.contains("mixedDisplayContent"), "No MCB icon on HTTP page");
ok(!classList.contains("mixedDisplayContentLoadedActiveBlocked"), "No MCB icon on HTTP page");
ok(!classList.contains("mixedContent"), "No MCB icon on HTTP page");
} else {
// Make sure the identity box UI has the correct mixedcontent states and icons
is(classList.contains("mixedActiveContent"), activeLoaded,
@ -817,8 +816,6 @@ function assertMixedContentBlockingState(tabbrowser, states = {}) {
"identityBox has expected class for passiveLoaded && !(activeLoaded || activeBlocked)");
is(classList.contains("mixedDisplayContentLoadedActiveBlocked"), passiveLoaded && activeBlocked,
"identityBox has expected class for passiveLoaded && activeBlocked");
is (classList.contains("mixedContent"), activeBlocked || activeLoaded || passiveLoaded,
"identityBox has expected class for mixed content");
if (activeLoaded) {
is(identityBoxImage, "url(\"chrome://browser/skin/identity-mixed-active-loaded.svg\")",

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

@ -139,7 +139,7 @@
<!-- Active Mixed Content Blocking Disabled -->
<description when-mixedcontent="active-loaded">&identity.description.activeLoaded;</description>
<description when-mixedcontent="active-loaded">&identity.description.activeLoaded2;</description>
<description when-mixedcontent="active-loaded">&identity.description.activeLoaded2; <label observes="identity-popup-mcb-learn-more"/></description>
<!-- Buttons to enable/disable mixed content blocking. -->
<button when-mixedcontent="active-blocked"

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

@ -12,3 +12,4 @@ support-files =
[browser_ext_tabs_query.js]
[browser_ext_tabs_update.js]
[browser_ext_windows_update.js]
[browser_ext_contentscript_connect.js]

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

@ -0,0 +1,58 @@
add_task(function* () {
let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://mochi.test:8888/");
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"permissions": ["tabs"]
},
background: function() {
var ports_received = 0;
var port_messages_received = 0;
browser.runtime.onConnect.addListener((port) => {
browser.test.assertTrue(!!port, "port1 received");
ports_received++;
browser.test.assertEq(1, ports_received, "1 port received");
port.onMessage.addListener((msg, sender) => {
browser.test.assertEq("port message", msg, "listener1 port message received");
port_messages_received++
browser.test.assertEq(1, port_messages_received, "1 port message received");
})
});
browser.runtime.onConnect.addListener((port) => {
browser.test.assertTrue(!!port, "port2 received");
ports_received++;
browser.test.assertEq(2, ports_received, "2 ports received");
port.onMessage.addListener((msg, sender) => {
browser.test.assertEq("port message", msg, "listener2 port message received");
port_messages_received++
browser.test.assertEq(2, port_messages_received, "2 port messages received");
browser.test.notifyPass("contentscript_connect.pass");
});
});
browser.tabs.executeScript({ file: "script.js" });
},
files: {
"script.js": function() {
var port = browser.runtime.connect();
port.postMessage("port message");
}
}
});
yield extension.startup();
yield extension.awaitFinish("contentscript_connect.pass");
yield extension.unload();
yield BrowserTestUtils.removeTab(tab);
});

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

@ -401,6 +401,9 @@ loop.conversationViews = (function(mozL10n) {
case FAILURE_DETAILS.NO_MEDIA:
case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
return mozL10n.get("no_media_failure_message");
case FAILURE_DETAILS.TOS_FAILURE:
return mozL10n.get("tos_failure_message",
{ clientShortname: mozL10n.get("clientShortname2") });
default:
return mozL10n.get("generic_failure_message");
}

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

@ -401,6 +401,9 @@ loop.conversationViews = (function(mozL10n) {
case FAILURE_DETAILS.NO_MEDIA:
case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
return mozL10n.get("no_media_failure_message");
case FAILURE_DETAILS.TOS_FAILURE:
return mozL10n.get("tos_failure_message",
{ clientShortname: mozL10n.get("clientShortname2") });
default:
return mozL10n.get("generic_failure_message");
}

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

@ -450,21 +450,6 @@ loop.shared.actions = (function() {
failedJoinRequest: Boolean
}),
/**
* Sets up the room information when it is received.
* XXX: should move to some roomActions module - refs bug 1079284
*
* @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
*/
SetupRoomInfo: Action.define("setupRoomInfo", {
// roomContextUrls: Array - Optional.
// roomDescription: String - Optional.
// roomName: String - Optional.
roomToken: String,
roomUrl: String,
socialShareProviders: Array
}),
/**
* Updates the room information when it is received.
* XXX: should move to some roomActions module - refs bug 1079284
@ -472,11 +457,15 @@ loop.shared.actions = (function() {
* @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
*/
UpdateRoomInfo: Action.define("updateRoomInfo", {
// description: String - Optional.
// roomName: String - Optional.
roomUrl: String
// urls: Array - Optional.
// participants: Array - Optional.
// roomContextUrls: Array - Optional.
// See https://wiki.mozilla.org/Loop/Architecture/Context#Format_of_context.value
// roomDescription: String - Optional.
// roomInfoFailure: String - Optional.
// roomName: String - Optional.
// roomState: String - Optional.
roomUrl: String
// socialShareProviders: Array - Optional.
}),
/**

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

@ -49,12 +49,13 @@ loop.store.ActiveRoomStore = (function() {
var ROOM_INFO_FAILURES = loop.shared.utils.ROOM_INFO_FAILURES;
var OPTIONAL_ROOMINFO_FIELDS = {
urls: "roomContextUrls",
description: "roomDescription",
participants: "participants",
roomContextUrls: "roomContextUrls",
roomDescription: "roomDescription",
roomInfoFailure: "roomInfoFailure",
roomName: "roomName",
roomState: "roomState"
roomState: "roomState",
socialShareProviders: "socialShareProviders"
};
/**
@ -142,8 +143,6 @@ loop.store.ActiveRoomStore = (function() {
receivingScreenShare: false,
// Any urls (aka context) associated with the room.
roomContextUrls: null,
// The roomCryptoKey to decode the context data if necessary.
roomCryptoKey: null,
// The description for a room as stored in the context data.
roomDescription: null,
// Room information failed to be obtained for a reason. See ROOM_INFO_FAILURES.
@ -237,7 +236,6 @@ loop.store.ActiveRoomStore = (function() {
this.dispatcher.register(this, [
"roomFailure",
"retryAfterRoomFailure",
"setupRoomInfo",
"updateRoomInfo",
"gotMediaPermission",
"joinRoom",
@ -262,6 +260,15 @@ loop.store.ActiveRoomStore = (function() {
"connectionStatus",
"mediaConnected"
]);
this._onUpdateListener = this._handleRoomUpdate.bind(this);
this._onDeleteListener = this._handleRoomDelete.bind(this);
this._onSocialShareUpdate = this._handleSocialShareUpdate.bind(this);
this._mozLoop.rooms.on("update:" + this._storeState.roomToken, this._onUpdateListener);
this._mozLoop.rooms.on("delete:" + this._storeState.roomToken, this._onDeleteListener);
window.addEventListener("LoopShareWidgetChanged", this._onSocialShareUpdate);
window.addEventListener("LoopSocialProvidersChanged", this._onSocialShareUpdate);
},
/**
@ -278,13 +285,14 @@ loop.store.ActiveRoomStore = (function() {
return;
}
this._registerPostSetupActions();
this.setStoreState({
roomState: ROOM_STATES.GATHER,
roomToken: actionData.roomToken,
windowId: actionData.windowId
});
this._registerPostSetupActions();
// Get the window data from the mozLoop api.
this._mozLoop.rooms.get(actionData.roomToken,
function(error, roomData) {
@ -296,12 +304,12 @@ loop.store.ActiveRoomStore = (function() {
return;
}
this.dispatchAction(new sharedActions.SetupRoomInfo({
this.dispatchAction(new sharedActions.UpdateRoomInfo({
participants: roomData.participants,
roomToken: actionData.roomToken,
roomContextUrls: roomData.decryptedContext.urls,
roomDescription: roomData.decryptedContext.description,
roomName: roomData.decryptedContext.roomName,
roomState: ROOM_STATES.READY,
roomUrl: roomData.roomUrl,
socialShareProviders: this._mozLoop.getSocialShareProviders()
}));
@ -326,23 +334,18 @@ loop.store.ActiveRoomStore = (function() {
return;
}
this._registerPostSetupActions();
this.setStoreState({
roomToken: actionData.token,
roomState: ROOM_STATES.GATHER,
roomCryptoKey: actionData.cryptoKey
roomToken: actionData.token,
standalone: true
});
this._mozLoop.rooms.on("update:" + actionData.roomToken,
this._handleRoomUpdate.bind(this));
this._mozLoop.rooms.on("delete:" + actionData.roomToken,
this._handleRoomDelete.bind(this));
this._registerPostSetupActions();
this._getRoomDataForStandalone();
this._getRoomDataForStandalone(actionData.cryptoKey);
},
_getRoomDataForStandalone: function() {
_getRoomDataForStandalone: function(roomCryptoKey) {
this._mozLoop.rooms.get(this._storeState.roomToken, function(err, result) {
if (err) {
this.dispatchAction(new sharedActions.RoomFailure({
@ -353,15 +356,14 @@ loop.store.ActiveRoomStore = (function() {
}
var roomInfoData = new sharedActions.UpdateRoomInfo({
// If we've got this far, then we want to go to the ready state
// regardless of success of failure. This is because failures of
// crypto don't stop the user using the room, they just stop
// us putting up the information.
roomState: ROOM_STATES.READY,
roomUrl: result.roomUrl
});
// If we've got this far, then we want to go to the ready state
// regardless of success of failure. This is because failures of
// crypto don't stop the user using the room, they just stop
// us putting up the information.
roomInfoData.roomState = ROOM_STATES.READY;
if (!result.context && !result.roomName) {
roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.NO_DATA;
this.dispatcher.dispatch(roomInfoData);
@ -381,8 +383,6 @@ loop.store.ActiveRoomStore = (function() {
return;
}
var roomCryptoKey = this.getStoreState("roomCryptoKey");
if (!roomCryptoKey) {
roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.NO_CRYPTO_KEY;
this.dispatcher.dispatch(roomInfoData);
@ -395,8 +395,8 @@ loop.store.ActiveRoomStore = (function() {
.then(function(decryptedResult) {
var realResult = JSON.parse(decryptedResult);
roomInfoData.description = realResult.description;
roomInfoData.urls = realResult.urls;
roomInfoData.roomDescription = realResult.description;
roomInfoData.roomContextUrls = realResult.urls;
roomInfoData.roomName = realResult.roomName;
dispatcher.dispatch(roomInfoData);
@ -407,39 +407,6 @@ loop.store.ActiveRoomStore = (function() {
}.bind(this));
},
/**
* Handles the setupRoomInfo action. Sets up the initial room data and
* sets the state to `READY`.
*
* @param {sharedActions.SetupRoomInfo} actionData
*/
setupRoomInfo: function(actionData) {
if (this._onUpdateListener) {
console.error("Room info already set up!");
return;
}
this.setStoreState({
participants: actionData.participants,
roomContextUrls: actionData.roomContextUrls,
roomDescription: actionData.roomDescription,
roomName: actionData.roomName,
roomState: ROOM_STATES.READY,
roomToken: actionData.roomToken,
roomUrl: actionData.roomUrl,
socialShareProviders: actionData.socialShareProviders
});
this._onUpdateListener = this._handleRoomUpdate.bind(this);
this._onDeleteListener = this._handleRoomDelete.bind(this);
this._onSocialShareUpdate = this._handleSocialShareUpdate.bind(this);
this._mozLoop.rooms.on("update:" + actionData.roomToken, this._onUpdateListener);
this._mozLoop.rooms.on("delete:" + actionData.roomToken, this._onDeleteListener);
window.addEventListener("LoopShareWidgetChanged", this._onSocialShareUpdate);
window.addEventListener("LoopSocialProvidersChanged", this._onSocialShareUpdate);
},
/**
* Handles the updateRoomInfo action. Updates the room data.
*

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

@ -917,25 +917,38 @@ loop.OTSdkDriver = (function() {
},
_onOTException: function(event) {
if (event.code === OT.ExceptionCodes.UNABLE_TO_PUBLISH &&
event.message === "GetUserMedia") {
// We free up the publisher here in case the store wants to try
// grabbing the media again.
if (this.publisher) {
this.publisher.off("accessAllowed accessDenied accessDialogOpened streamCreated");
this.publisher.destroy();
delete this.publisher;
delete this._mockPublisherEl;
}
this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
}));
} else if (event.code === OT.ExceptionCodes.UNABLE_TO_PUBLISH) {
// We need to log the message so that we can understand where the exception
// is coming from. Potentially a temporary addition.
this._notifyMetricsEvent("sdk.exception." + event.code + "." + event.message);
} else {
this._notifyMetricsEvent("sdk.exception." + event.code);
switch (event.code) {
case OT.ExceptionCodes.UNABLE_TO_PUBLISH:
if (event.message === "GetUserMedia") {
// We free up the publisher here in case the store wants to try
// grabbing the media again.
if (this.publisher) {
this.publisher.off("accessAllowed accessDenied accessDialogOpened streamCreated");
this.publisher.destroy();
delete this.publisher;
delete this._mockPublisherEl;
}
this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
}));
// No exception logging as this is a handled event.
} else {
// We need to log the message so that we can understand where the exception
// is coming from. Potentially a temporary addition.
this._notifyMetricsEvent("sdk.exception." + event.code + "." + event.message);
}
break;
case OT.ExceptionCodes.TERMS_OF_SERVICE_FAILURE:
this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.TOS_FAILURE
}));
// We still need to log the exception so that the server knows why this
// attempt failed.
this._notifyMetricsEvent("sdk.exception." + event.code);
break;
default:
this._notifyMetricsEvent("sdk.exception." + event.code);
break;
}
},

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

@ -80,6 +80,9 @@ var inChrome = typeof Components != "undefined" && "utils" in Components;
COULD_NOT_CONNECT: "reason-could-not-connect",
NETWORK_DISCONNECTED: "reason-network-disconnected",
EXPIRED_OR_INVALID: "reason-expired-or-invalid",
// TOS_FAILURE reflects the sdk error code 1026:
// https://tokbox.com/developer/sdks/js/reference/ExceptionEvent.html
TOS_FAILURE: "reason-tos-failure",
UNKNOWN: "reason-unknown"
};

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

@ -43,6 +43,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
return mozL10n.get("rooms_media_denied_message");
case FAILURE_DETAILS.EXPIRED_OR_INVALID:
return mozL10n.get("rooms_unavailable_notification_message");
case FAILURE_DETAILS.TOS_FAILURE:
return mozL10n.get("tos_failure_message",
{ clientShortname: mozL10n.get("clientShortname2") });
default:
return mozL10n.get("status_error");
}
@ -52,7 +55,8 @@ loop.standaloneRoomViews = (function(mozL10n) {
* This renders a retry button if one is necessary.
*/
renderRetryButton: function() {
if (this.props.failureReason === FAILURE_DETAILS.EXPIRED_OR_INVALID) {
if (this.props.failureReason === FAILURE_DETAILS.EXPIRED_OR_INVALID ||
this.props.failureReason === FAILURE_DETAILS.TOS_FAILURE) {
return null;
}
@ -593,6 +597,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
});
return {
StandaloneRoomFailureView: StandaloneRoomFailureView,
StandaloneRoomFooter: StandaloneRoomFooter,
StandaloneRoomHeader: StandaloneRoomHeader,
StandaloneRoomInfoArea: StandaloneRoomInfoArea,

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

@ -43,6 +43,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
return mozL10n.get("rooms_media_denied_message");
case FAILURE_DETAILS.EXPIRED_OR_INVALID:
return mozL10n.get("rooms_unavailable_notification_message");
case FAILURE_DETAILS.TOS_FAILURE:
return mozL10n.get("tos_failure_message",
{ clientShortname: mozL10n.get("clientShortname2") });
default:
return mozL10n.get("status_error");
}
@ -52,7 +55,8 @@ loop.standaloneRoomViews = (function(mozL10n) {
* This renders a retry button if one is necessary.
*/
renderRetryButton: function() {
if (this.props.failureReason === FAILURE_DETAILS.EXPIRED_OR_INVALID) {
if (this.props.failureReason === FAILURE_DETAILS.EXPIRED_OR_INVALID ||
this.props.failureReason === FAILURE_DETAILS.TOS_FAILURE) {
return null;
}
@ -593,6 +597,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
});
return {
StandaloneRoomFailureView: StandaloneRoomFailureView,
StandaloneRoomFooter: StandaloneRoomFooter,
StandaloneRoomHeader: StandaloneRoomHeader,
StandaloneRoomInfoArea: StandaloneRoomInfoArea,

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

@ -3,6 +3,10 @@ conversation_has_ended=Your conversation has ended.
generic_failure_message=We're having technical difficulties…
generic_failure_with_reason2=You can try again or email a link to be reached at later.
generic_failure_no_reason2=Would you like to try again?
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname2}}
## as this will be replaced by the shortname.
tos_failure_message={{clientShortname}} is not available in your country.
retry_call_button=Retry
unable_retrieve_call_info=Unable to retrieve conversation information.
hangup_button_title=Hang up

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

@ -307,6 +307,16 @@ describe("loop.conversationViews", function () {
expect(message.textContent).eql("contact_unavailable_title");
});
it("should display a ToS failure message for the ToS failure reason", function() {
view = mountTestComponent({
failureReason: FAILURE_DETAILS.TOS_FAILURE
});
var message = view.getDOMNode().querySelector(".failure-info-message");
expect(message.textContent).eql("tos_failure_message");
});
it("should display a generic unavailable message if the contact doesn't have a display name", function() {
view = mountTestComponent({
contact: {

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

@ -326,19 +326,29 @@ describe("loop.store.ActiveRoomStore", function () {
);
});
it("should set the state to `GATHER`",
function() {
store.setupWindowData(new sharedActions.SetupWindowData({
windowId: "42",
type: "room",
roomToken: fakeToken
}));
it("should set the state to `GATHER`", function() {
store.setupWindowData(new sharedActions.SetupWindowData({
windowId: "42",
type: "room",
roomToken: fakeToken
}));
expect(store.getStoreState()).to.have.property(
"roomState", ROOM_STATES.GATHER);
});
expect(store.getStoreState()).to.have.property(
"roomState", ROOM_STATES.GATHER);
});
it("should dispatch an SetupRoomInfo action if the get is successful",
it("should store the room token and window id", function() {
store.setupWindowData(new sharedActions.SetupWindowData({
windowId: "42",
type: "room",
roomToken: fakeToken
}));
expect(store.getStoreState().windowId).eql("42");
expect(store.getStoreState().roomToken).eql(fakeToken);
});
it("should dispatch an UpdateRoomInfo action if the get is successful",
function() {
store.setupWindowData(new sharedActions.SetupWindowData({
windowId: "42",
@ -348,12 +358,12 @@ describe("loop.store.ActiveRoomStore", function () {
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.SetupRoomInfo({
new sharedActions.UpdateRoomInfo({
roomContextUrls: undefined,
roomDescription: undefined,
participants: [],
roomToken: fakeToken,
roomName: fakeRoomData.decryptedContext.roomName,
roomState: ROOM_STATES.READY,
roomUrl: fakeRoomData.roomUrl,
socialShareProviders: []
}));
@ -551,8 +561,11 @@ describe("loop.store.ActiveRoomStore", function () {
store.fetchServerData(fetchServerAction);
var expectedData = _.extend({
roomContextUrls: roomContext.urls,
roomDescription: roomContext.description,
roomName: roomContext.roomName,
roomState: ROOM_STATES.READY
}, roomContext, expectedDetails);
}, expectedDetails);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
@ -586,47 +599,18 @@ describe("loop.store.ActiveRoomStore", function () {
});
});
describe("#setupRoomInfo", function() {
var fakeRoomInfo;
beforeEach(function() {
fakeRoomInfo = {
roomName: "Its a room",
roomToken: "fakeToken",
roomUrl: "http://invalid",
socialShareProviders: []
};
});
it("should set the state to READY", function() {
store.setupRoomInfo(new sharedActions.SetupRoomInfo(fakeRoomInfo));
expect(store._storeState.roomState).eql(ROOM_STATES.READY);
});
it("should save the room information", function() {
store.setupRoomInfo(new sharedActions.SetupRoomInfo(fakeRoomInfo));
var state = store.getStoreState();
expect(state.roomName).eql(fakeRoomInfo.roomName);
expect(state.roomToken).eql(fakeRoomInfo.roomToken);
expect(state.roomUrl).eql(fakeRoomInfo.roomUrl);
expect(state.socialShareProviders).eql([]);
});
});
describe("#updateRoomInfo", function() {
var fakeRoomInfo;
beforeEach(function() {
fakeRoomInfo = {
roomName: "Its a room",
roomUrl: "http://invalid",
urls: [{
roomContextUrls: [{
description: "fake site",
location: "http://invalid.com",
thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
}]
}],
roomName: "Its a room",
roomUrl: "http://invalid"
};
});
@ -636,7 +620,7 @@ describe("loop.store.ActiveRoomStore", function () {
var state = store.getStoreState();
expect(state.roomName).eql(fakeRoomInfo.roomName);
expect(state.roomUrl).eql(fakeRoomInfo.roomUrl);
expect(state.roomContextUrls).eql(fakeRoomInfo.urls);
expect(state.roomContextUrls).eql(fakeRoomInfo.roomContextUrls);
});
});
@ -1602,11 +1586,10 @@ describe("loop.store.ActiveRoomStore", function () {
describe("Events", function() {
describe("update:{roomToken}", function() {
beforeEach(function() {
store.setupRoomInfo(new sharedActions.SetupRoomInfo({
roomName: "Its a room",
store.setupWindowData(new sharedActions.SetupWindowData({
roomToken: "fakeToken",
roomUrl: "http://invalid",
socialShareProviders: []
type: "room",
windowId: "42"
}));
});
@ -1667,11 +1650,11 @@ describe("loop.store.ActiveRoomStore", function () {
};
beforeEach(function() {
store.setupRoomInfo(new sharedActions.SetupRoomInfo(
_.extend(fakeRoomData, {
socialShareProviders: []
})
));
store.setupWindowData(new sharedActions.SetupWindowData({
roomToken: "fakeToken",
type: "room",
windowId: "42"
}));
});
it("should disconnect all room connections", function() {

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

@ -68,6 +68,7 @@ describe("loop.OTSdkDriver", function () {
window.OT = {
ExceptionCodes: {
CONNECT_FAILED: 1006,
TERMS_OF_SERVICE_FAILURE: 1026,
UNABLE_TO_PUBLISH: 1500
}
};
@ -1599,6 +1600,38 @@ describe("loop.OTSdkDriver", function () {
}));
});
});
describe("ToS Failure", function() {
it("should dispatch a ConnectionFailure action", function() {
sdk.trigger("exception", {
code: OT.ExceptionCodes.TERMS_OF_SERVICE_FAILURE,
message: "Fake"
});
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.TOS_FAILURE
}));
});
it("should notify metrics", function() {
sdk.trigger("exception", {
code: OT.ExceptionCodes.TERMS_OF_SERVICE_FAILURE,
message: "Fake"
});
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ConnectionStatus({
event: "sdk.exception." + OT.ExceptionCodes.TERMS_OF_SERVICE_FAILURE,
state: "starting",
connections: 0,
sendStreams: 0,
recvStreams: 0
}));
});
});
});
});

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

@ -17,7 +17,7 @@ describe("loop.standaloneRoomViews", function() {
var fixtures = document.querySelector("#fixtures");
var sandbox, dispatcher, activeRoomStore, dispatch;
var clock, fakeWindow;
var clock, fakeWindow, view;
beforeEach(function() {
sandbox = sinon.sandbox.create();
@ -63,6 +63,7 @@ describe("loop.standaloneRoomViews", function() {
sandbox.restore();
clock.restore();
React.unmountComponentAtNode(fixtures);
view = null;
});
describe("StandaloneRoomHeader", function() {
@ -75,7 +76,7 @@ describe("loop.standaloneRoomViews", function() {
}
it("should dispatch a RecordClick action when the support link is clicked", function() {
var view = mountTestComponent();
view = mountTestComponent();
TestUtils.Simulate.click(view.getDOMNode().querySelector("a"));
@ -87,13 +88,93 @@ describe("loop.standaloneRoomViews", function() {
});
});
describe("StandaloneRoomFailureView", function() {
function mountTestComponent(extraProps) {
var props = _.extend({
dispatcher: dispatcher
}, extraProps);
return TestUtils.renderIntoDocument(
React.createElement(
loop.standaloneRoomViews.StandaloneRoomFailureView, props));
}
beforeEach(function() {
activeRoomStore.setStoreState({ roomState: ROOM_STATES.FAILED });
});
it("should display a status error message if not reason is supplied", function() {
view = mountTestComponent();
expect(view.getDOMNode().querySelector(".failed-room-message").textContent)
.eql("status_error");
});
it("should display a denied message on MEDIA_DENIED", function() {
view = mountTestComponent({ failureReason: FAILURE_DETAILS.MEDIA_DENIED });
expect(view.getDOMNode().querySelector(".failed-room-message").textContent)
.eql("rooms_media_denied_message");
});
it("should display a denied message on NO_MEDIA", function() {
view = mountTestComponent({ failureReason: FAILURE_DETAILS.NO_MEDIA });
expect(view.getDOMNode().querySelector(".failed-room-message").textContent)
.eql("rooms_media_denied_message");
});
it("should display an unavailable message on EXPIRED_OR_INVALID", function() {
view = mountTestComponent({ failureReason: FAILURE_DETAILS.EXPIRED_OR_INVALID });
expect(view.getDOMNode().querySelector(".failed-room-message").textContent)
.eql("rooms_unavailable_notification_message");
});
it("should display an tos failure message on TOS_FAILURE", function() {
view = mountTestComponent({ failureReason: FAILURE_DETAILS.TOS_FAILURE });
expect(view.getDOMNode().querySelector(".failed-room-message").textContent)
.eql("tos_failure_message");
});
it("should not display a retry button when the failure reason is expired or invalid", function() {
view = mountTestComponent({ failureReason: FAILURE_DETAILS.EXPIRED_OR_INVALID });
expect(view.getDOMNode().querySelector(".btn-info")).eql(null);
});
it("should not display a retry button when the failure reason is tos failure", function() {
view = mountTestComponent({ failureReason: FAILURE_DETAILS.TOS_FAILURE });
expect(view.getDOMNode().querySelector(".btn-info")).eql(null);
});
it("should display a retry button for any other reason", function() {
view = mountTestComponent({ failureReason: FAILURE_DETAILS.NO_MEDIA });
expect(view.getDOMNode().querySelector(".btn-info")).not.eql(null);
});
it("should dispatch a RetryAfterRoomFailure action when the retry button is pressed", function() {
view = mountTestComponent({ failureReason: FAILURE_DETAILS.NO_MEDIA });
var button = view.getDOMNode().querySelector(".btn-info");
TestUtils.Simulate.click(button);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.RetryAfterRoomFailure());
});
});
describe("StandaloneRoomInfoArea in fixture", function() {
it("should dispatch a RecordClick action when the tile is clicked", function(done) {
// Point the iframe to a page that will auto-"click"
loop.config.tilesIframeUrl = "data:text/html,<script>parent.postMessage('tile-click', '*');</script>";
// Render the iframe into the fixture to cause it to load
var view = React.render(
view = React.render(
React.createElement(
loop.standaloneRoomViews.StandaloneRoomInfoArea, {
activeRoomStore: activeRoomStore,
@ -135,7 +216,7 @@ describe("loop.standaloneRoomViews", function() {
}));
}
function expectActionDispatched(view) {
function expectActionDispatched() {
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWithExactly(dispatch,
sinon.match.instanceOf(sharedActions.SetupStreamElements));
@ -144,7 +225,7 @@ describe("loop.standaloneRoomViews", function() {
describe("#componentWillUpdate", function() {
it("should set document.title to roomName and brand name when the READY state is dispatched", function() {
activeRoomStore.setStoreState({roomName: "fakeName", roomState: ROOM_STATES.INIT});
var view = mountTestComponent();
view = mountTestComponent();
activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
expect(fakeWindow.document.title).to.equal("fakeName — clientShortname2");
@ -153,7 +234,7 @@ describe("loop.standaloneRoomViews", function() {
it("should dispatch a `SetupStreamElements` action when the MEDIA_WAIT state " +
"is entered", function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
var view = mountTestComponent();
view = mountTestComponent();
activeRoomStore.setStoreState({roomState: ROOM_STATES.MEDIA_WAIT});
@ -163,7 +244,7 @@ describe("loop.standaloneRoomViews", function() {
it("should dispatch a `SetupStreamElements` action on MEDIA_WAIT state is " +
"re-entered", function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.ENDED});
var view = mountTestComponent();
view = mountTestComponent();
activeRoomStore.setStoreState({roomState: ROOM_STATES.MEDIA_WAIT});
@ -172,8 +253,6 @@ describe("loop.standaloneRoomViews", function() {
});
describe("#componentDidUpdate", function() {
var view;
beforeEach(function() {
view = mountTestComponent();
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINING});
@ -223,8 +302,6 @@ describe("loop.standaloneRoomViews", function() {
});
describe("#componentWillReceiveProps", function() {
var view;
beforeEach(function() {
view = mountTestComponent();
@ -280,8 +357,6 @@ describe("loop.standaloneRoomViews", function() {
});
describe("#publishStream", function() {
var view;
beforeEach(function() {
view = mountTestComponent();
view.setState({
@ -314,8 +389,6 @@ describe("loop.standaloneRoomViews", function() {
});
describe("#render", function() {
var view;
beforeEach(function() {
view = mountTestComponent();
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINING});
@ -424,35 +497,11 @@ describe("loop.standaloneRoomViews", function() {
});
describe("Failed room message", function() {
beforeEach(function() {
it("should display the StandaloneRoomFailureView", function() {
activeRoomStore.setStoreState({ roomState: ROOM_STATES.FAILED });
});
it("should display a failed room message on FAILED", function() {
expect(view.getDOMNode().querySelector(".failed-room-message"))
.not.eql(null);
});
it("should display a retry button", function() {
expect(view.getDOMNode().querySelector(".btn-info")).not.eql(null);
});
it("should not display a retry button when the failure reason is expired or invalid", function() {
activeRoomStore.setStoreState({
failureReason: FAILURE_DETAILS.EXPIRED_OR_INVALID
});
expect(view.getDOMNode().querySelector(".btn-info")).eql(null);
});
it("should dispatch a RetryAfterRoomFailure action when the retry button is pressed", function() {
var button = view.getDOMNode().querySelector(".btn-info");
TestUtils.Simulate.click(button);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.RetryAfterRoomFailure());
TestUtils.findRenderedComponentWithType(view,
loop.standaloneRoomViews.StandaloneRoomFailureView);
});
});

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

@ -686,8 +686,9 @@ Cookies.prototype = {
host = "." + host;
}
// Fallback: expire in 1h
let expireTime = (Date.now() + 3600 * 1000) * 1000;
// Fallback: expire in 1h (NB: time is in seconds since epoch, so we have
// to divide the result of Date.now() (which is in milliseconds) by 1000).
let expireTime = Math.floor(Date.now() / 1000) + 3600;
try {
expireTime = this.ctypesKernelHelpers.fileTimeToSecondsSinceEpoch(Number(expireTimeHi),
Number(expireTimeLo));
@ -751,10 +752,14 @@ function getTypedURLs(registryKeyPath) {
try {
let hi = parseInt(urlTimeHex.slice(0, 4).join(''), 16);
let lo = parseInt(urlTimeHex.slice(4, 8).join(''), 16);
// Convert to seconds since epoch:
timeTyped = cTypes.fileTimeToSecondsSinceEpoch(hi, lo);
// Callers expect PRTime, which is microseconds since epoch:
timeTyped *= 1000 * 1000;
} catch (ex) {}
} catch (ex) {
// Ignore conversion exceptions. Callers will have to deal
// with the fallback value (0).
}
}
}
typedURLs.set(url, timeTyped);
@ -862,9 +867,16 @@ WindowsVaultFormPasswords.prototype = {
}
let password = credential.contents.pAuthenticatorElement.contents.itemValue.readString();
let creation = ctypesKernelHelpers.
let creation = Date.now();
try {
// login manager wants time in milliseconds since epoch, so convert
// to seconds since epoch and multiply to get milliseconds:
creation = ctypesKernelHelpers.
fileTimeToSecondsSinceEpoch(item.contents.highLastModified,
item.contents.lowLastModified) * 1000;
} catch (ex) {
// Ignore exceptions in the dates and just create the login for right now.
}
// create a new login
let login = {
username, password,

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

@ -280,6 +280,9 @@ generic_contact_unavailable_title=This person is unavailable.
generic_failure_message=We're having technical difficulties…
generic_failure_with_reason2=You can try again or email a link to be reached at later.
generic_failure_no_reason2=Would you like to try again?
## LOCALIZATION NOTE (tos_failure_message): Don't translate {{clientShortname2}}
## as this will be replaced by the shortname.
tos_failure_message={{clientShortname}} is not available in your country.
## LOCALIZATION NOTE (contact_offline_title): Title which is displayed when the
## contact is offline.

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

@ -33,14 +33,15 @@ cmd_addFoundEngine=Add "%S"
# grouped in a submenu using cmd_addFoundEngineMenu as a label.
cmd_addFoundEngineMenu=Add search engine
# LOCALIZATION NOTE (searchForKeywordsWith):
# LOCALIZATION NOTE (searchForSomethingWith):
# This string is used to build the header above the list of one-click
# search providers: "Search for <user-typed keywords> with:"
searchForKeywordsWith=Search for %S with:
# search providers: "Search for <user-typed string> with:"
# NB: please leave the <span> and its class exactly as it is in English.
searchForSomethingWith=Search for <span class='contentSearchSearchWithHeaderSearchText'></span> with:
# LOCALIZATION NOTE (searchWithHeader):
# The wording of this string should be as close as possible to
# searchForKeywordsWith. This string will be used when the user
# searchForSomethingWith. This string will be used when the user
# has not typed anything.
searchWithHeader=Search with:

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

@ -114,7 +114,7 @@ this.ContentSearch = {
}
this._searchSuggestionUIStrings = {};
let searchBundle = Services.strings.createBundle("chrome://browser/locale/search.properties");
let stringNames = ["searchHeader", "searchPlaceholder", "searchForKeywordsWith",
let stringNames = ["searchHeader", "searchPlaceholder", "searchForSomethingWith",
"searchWithHeader", "searchSettings"];
for (let name of stringNames) {
this._searchSuggestionUIStrings[name] = searchBundle.GetStringFromName(name);

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

@ -29,7 +29,7 @@
--toolbarbutton-active-bordercolor: rgb(154,154,154);
--toolbarbutton-active-background: rgba(154,154,154,.5) linear-gradient(hsla(0,0%,100%,.7), hsla(0,0%,100%,.4));
--toolbarbutton-checkedhover-backgroundcolor: rgba(90%,90%,90%,.4);
--toolbarbutton-checkedhover-backgroundcolor: rgba(154,154,154,.15);
--identity-box-verified-background-color: #fff;
@ -1598,7 +1598,7 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
}
#TabsToolbar .toolbarbutton-1 {
margin-bottom: var(--navbar-tab-toolbar-highlight-overlap);
margin-bottom: var(--tab-toolbar-navbar-overlap);
}
#alltabs-button {

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

@ -2805,7 +2805,7 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker {
}
#TabsToolbar .toolbarbutton-1 {
margin-bottom: var(--navbar-tab-toolbar-highlight-overlap);
margin-bottom: var(--tab-toolbar-navbar-overlap);
}
#TabsToolbar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {

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

@ -33,7 +33,7 @@
--toolbarbutton-active-bordercolor: rgba(0,0,0,.15);
--toolbarbutton-active-boxshadow: 0 0 0 1px rgba(0,0,0,.15) inset;
--toolbarbutton-checkedhover-backgroundcolor: rgba(0,0,0,.15);
--toolbarbutton-checkedhover-backgroundcolor: rgba(0,0,0,.1);
--identity-box-verified-background-color: #fff;
@ -59,6 +59,8 @@
--toolbarbutton-active-background: rgba(255,255,255,.3);
--toolbarbutton-active-bordercolor: rgba(255,255,255,.3);
--toolbarbutton-active-boxshadow: 0 0 0 1px rgba(255,255,255,.3) inset;
--toolbarbutton-checkedhover-backgroundcolor: rgba(255,255,255,.2);
}
#menubar-items {
@ -899,7 +901,7 @@ toolbarbutton[constrain-size="true"][cui-areatype="toolbar"] > .toolbarbutton-ba
}
#TabsToolbar .toolbarbutton-1 {
margin-bottom: var(--navbar-tab-toolbar-highlight-overlap);
margin-bottom: var(--tab-toolbar-navbar-overlap);
}
#TabsToolbar .toolbarbutton-1:not([disabled=true]):hover,

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

@ -1,125 +0,0 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* 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/. */
package org.mozilla.gecko;
import org.mozilla.gecko.gfx.BitmapUtils;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.util.Log;
import android.widget.RemoteViews;
import java.text.NumberFormat;
public class AlertNotification
extends Notification
{
private static final String LOGTAG = "GeckoAlertNotification";
private final int mId;
private final int mIcon;
private final String mTitle;
private final String mText;
private final NotificationManager mNotificationManager;
private boolean mProgressStyle;
private double mPrevPercent = -1;
private String mPrevAlertText = "";
private static final double UPDATE_THRESHOLD = .01;
private final Context mContext;
public AlertNotification(Context aContext, int aNotificationId, int aIcon,
String aTitle, String aText, long aWhen, Uri aIconUri) {
super(aIcon, (aText.length() > 0) ? aText : aTitle, aWhen);
mIcon = aIcon;
mTitle = aTitle;
mText = aText;
mId = aNotificationId;
mContext = aContext;
mNotificationManager = (NotificationManager) aContext.getSystemService(Context.NOTIFICATION_SERVICE);
if (aIconUri == null || aIconUri.getScheme() == null)
return;
// Custom view
int layout = R.layout.notification_icon_text;
RemoteViews view = new RemoteViews(mContext.getPackageName(), layout);
try {
Bitmap bm = BitmapUtils.decodeUrl(aIconUri);
if (bm == null) {
Log.e(LOGTAG, "failed to decode icon");
return;
}
view.setImageViewBitmap(R.id.notification_image, bm);
view.setTextViewText(R.id.notification_title, mTitle);
if (mText.length() > 0) {
view.setTextViewText(R.id.notification_text, mText);
}
contentView = view;
} catch (Exception e) {
Log.e(LOGTAG, "failed to create bitmap", e);
}
}
public int getId() {
return mId;
}
public synchronized boolean isProgressStyle() {
return mProgressStyle;
}
public void show() {
mNotificationManager.notify(mId, this);
}
public void cancel() {
mNotificationManager.cancel(mId);
}
public synchronized void updateProgress(String aAlertText, long aProgress, long aProgressMax) {
if (!mProgressStyle) {
// Custom view
int layout = aAlertText.length() > 0 ? R.layout.notification_progress_text : R.layout.notification_progress;
RemoteViews view = new RemoteViews(mContext.getPackageName(), layout);
view.setImageViewResource(R.id.notification_image, mIcon);
view.setTextViewText(R.id.notification_title, mTitle);
contentView = view;
flags |= FLAG_ONGOING_EVENT | FLAG_ONLY_ALERT_ONCE;
mProgressStyle = true;
}
String text;
double percent = 0;
if (aProgressMax > 0)
percent = ((double)aProgress / (double)aProgressMax);
if (aAlertText.length() > 0)
text = aAlertText;
else
text = NumberFormat.getPercentInstance().format(percent);
if (mPrevAlertText.equals(text) && Math.abs(mPrevPercent - percent) < UPDATE_THRESHOLD)
return;
contentView.setTextViewText(R.id.notification_text, text);
contentView.setProgressBar(R.id.notification_progressbar, (int)aProgressMax, (int)aProgress, false);
// Update the notification
mNotificationManager.notify(mId, this);
mPrevPercent = percent;
mPrevAlertText = text;
}
}

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

@ -12,6 +12,8 @@ import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.net.Uri;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.util.Log;
import java.util.concurrent.ConcurrentHashMap;
@ -20,7 +22,7 @@ public class NotificationHandler {
private final ConcurrentHashMap<Integer, Notification>
mNotifications = new ConcurrentHashMap<Integer, Notification>();
private final Context mContext;
private final NotificationManager mNotificationManager;
private final NotificationManagerCompat mNotificationManager;
/**
* Notification associated with this service's foreground state.
@ -37,7 +39,7 @@ public class NotificationHandler {
public NotificationHandler(Context context) {
mContext = context;
mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager = NotificationManagerCompat.from(context);
}
/**
@ -48,7 +50,6 @@ public class NotificationHandler {
* @param aAlertTitle title of the notification
* @param aAlertText text of the notification
* @param contentIntent Intent used when the notification is clicked
* @param clearIntent Intent used when the notification is removed
*/
public void add(int notificationID, String aImageUrl, String aAlertTitle,
String aAlertText, PendingIntent contentIntent) {
@ -57,10 +58,14 @@ public class NotificationHandler {
Uri imageUri = Uri.parse(aImageUrl);
int icon = BitmapUtils.getResource(imageUri, R.drawable.ic_status_logo);
final AlertNotification notification = new AlertNotification(mContext, notificationID,
icon, aAlertTitle, aAlertText, System.currentTimeMillis(), imageUri);
notification.setLatestEventInfo(mContext, aAlertTitle, aAlertText, contentIntent);
Notification notification = new NotificationCompat.Builder(mContext)
.setContentTitle(aAlertTitle)
.setContentText(aAlertText)
.setSmallIcon(icon)
.setWhen(System.currentTimeMillis())
.setContentIntent(contentIntent)
.build();
mNotificationManager.notify(notificationID, notification);
mNotifications.put(notificationID, notification);
@ -70,7 +75,7 @@ public class NotificationHandler {
* Adds a notification.
*
* @param id the unique ID of the notification
* @param aNotification the Notification to add
* @param notification the Notification to add
*/
public void add(int id, Notification notification) {
mNotificationManager.notify(id, notification);
@ -90,19 +95,20 @@ public class NotificationHandler {
* @param aAlertText text of the notification
*/
public void update(int notificationID, long aProgress, long aProgressMax, String aAlertText) {
final Notification notification = mNotifications.get(notificationID);
Notification notification = mNotifications.get(notificationID);
if (notification == null) {
return;
}
if (notification instanceof AlertNotification) {
AlertNotification alert = (AlertNotification)notification;
alert.updateProgress(aAlertText, aProgress, aProgressMax);
}
notification = new NotificationCompat.Builder(mContext)
.setContentText(aAlertText)
.setSmallIcon(notification.icon)
.setWhen(notification.when)
.setContentIntent(notification.contentIntent)
.setProgress((int) aProgressMax, (int) aProgress, false)
.build();
if (mForegroundNotification == null && isOngoing(notification)) {
setForegroundNotification(notificationID, notification);
}
add(notificationID, notification);
}
/**
@ -148,26 +154,12 @@ public class NotificationHandler {
* @return whether the notification is ongoing
*/
public boolean isOngoing(Notification notification) {
if (notification != null && (isProgressStyle(notification) || ((notification.flags & Notification.FLAG_ONGOING_EVENT) > 0))) {
if (notification != null && (notification.flags & Notification.FLAG_ONGOING_EVENT) > 0) {
return true;
}
return false;
}
/**
* Helper method to determines whether a notification is an AlertNotification that is showing progress
* This method will be deprecated when AlertNotifications are removed (bug 893289).
*
* @param notification the notification to check
* @return whether the notification is an AlertNotification showing progress.
*/
private boolean isProgressStyle(Notification notification) {
if (notification instanceof AlertNotification) {
return ((AlertNotification)notification).isProgressStyle();
}
return false;
}
protected void setForegroundNotification(int id, Notification notification) {
mForegroundNotificationId = id;
mForegroundNotification = notification;

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

@ -46,7 +46,6 @@ show allResults
[middle] = \
org.mozilla.gecko.prompts.* \
org.mozilla.gecko.AlertNotification \
org.mozilla.gecko.FormAssistPopup \
org.mozilla.gecko.GeckoActivity \
org.mozilla.gecko.GeckoApp \

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

@ -156,7 +156,6 @@ gbjar.sources += [
'ActionModeCompat.java',
'ActionModeCompatView.java',
'ActivityHandlerHelper.java',
'AlertNotification.java',
'AndroidGamepadManager.java',
'animation/AnimationUtils.java',
'animation/AnimatorProxy.java',

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

@ -133,6 +133,7 @@ EventManager.prototype = {
let fireWithoutClone = this.fireWithoutClone.bind(this);
fireFunc.withoutClone = fireWithoutClone;
this.unregister = this.register(fireFunc);
this.registered = true;
}
this.callbacks.add(callback);
},
@ -601,4 +602,3 @@ this.ExtensionUtils = {
Messenger,
flushJarCache,
};