Merge fx-team to central, a=merge

This commit is contained in:
Wes Kocher 2015-08-11 15:42:21 -07:00
commit 573bbc33bd
37 changed files with 47678 additions and 49618 deletions

View File

@ -1864,6 +1864,8 @@ pref("browser.translation.engine", "bing");
// Telemetry settings.
// Determines if Telemetry pings can be archived locally.
pref("toolkit.telemetry.archive.enabled", true);
// Whether we enable opt-out Telemetry for a sample of the release population.
pref("toolkit.telemetry.optoutSample", true);
// Telemetry experiments settings.
pref("experiments.enabled", true);

View File

@ -49,7 +49,6 @@ const gXPInstallObserver = {
var options = {
displayURI: installInfo.originatingURI,
timeout: Date.now() + 30000,
removeOnDismissal: true,
};
let cancelInstallation = () => {

View File

@ -67,7 +67,6 @@ skip-if = !crashreporter
[browser_plugins_added_dynamically.js]
[browser_pluginnotification.js]
[browser_plugin_infolink.js]
skip-if = e10s # bug 1160166
[browser_plugin_reloading.js]
[browser_blocklist_content.js]
skip-if = !e10s

View File

@ -40,15 +40,11 @@ skip-if = e10s && debug # e10s/Linux/Debug Leaking docshells (bug 1150147)
[browser_social_chatwindowfocus.js]
skip-if = e10s # tab crash on data url used in this test
[browser_social_contextmenu.js]
skip-if = e10s # Bug 1072669 context menu relies on target element
[browser_social_errorPage.js]
[browser_social_flyout.js]
skip-if = e10s # when we backed out bug 1047603, this test broke.
[browser_social_isVisible.js]
[browser_social_marks.js]
skip-if = e10s && debug # Leaking docshells (bug 1150147)
[browser_social_multiprovider.js]
skip-if = e10s # Bug 1069162 - lots of orange
[browser_social_multiworker.js]
[browser_social_perwindowPB.js]
[browser_social_sidebar.js]

View File

@ -283,277 +283,9 @@ loop.shared.mixins = (function() {
/**
* Media setup mixin. Provides a common location for settings for the media
* elements and handling updates of the media containers.
* elements.
*/
var MediaSetupMixin = {
componentDidMount: function() {
this.resetDimensionsCache();
},
/**
* Resets the dimensions cache, e.g. for when the session is ended, and
* before a new session, so that we always ensure we see an update when a
* new session is started.
*/
resetDimensionsCache: function() {
this._videoDimensionsCache = {
local: {},
remote: {}
};
},
/**
* Whenever the dimensions change of a video stream, this function is called
* by `updateVideoDimensions` to store the new values and notifies the callee
* if the dimensions changed compared to the currently stored values.
*
* @param {String} which Type of video stream. May be 'local' or 'remote'
* @param {Object} newDimensions Object containing 'width' and 'height' properties
* @return {Boolean} `true` when the dimensions have changed,
* `false` if not
*/
_updateDimensionsCache: function(which, newDimensions) {
var cache = this._videoDimensionsCache[which];
var cacheKeys = Object.keys(cache);
var changed = false;
Object.keys(newDimensions).forEach(function(videoType) {
if (cacheKeys.indexOf(videoType) === -1) {
cache[videoType] = newDimensions[videoType];
cache[videoType].aspectRatio = this.getAspectRatio(cache[videoType]);
changed = true;
return;
}
if (cache[videoType].width !== newDimensions[videoType].width) {
cache[videoType].width = newDimensions[videoType].width;
changed = true;
}
if (cache[videoType].height !== newDimensions[videoType].height) {
cache[videoType].height = newDimensions[videoType].height;
changed = true;
}
if (changed) {
cache[videoType].aspectRatio = this.getAspectRatio(cache[videoType]);
}
}, this);
// Remove any streams that are no longer being published.
cacheKeys.forEach(function(videoType) {
if (!(videoType in newDimensions)) {
delete cache[videoType];
changed = true;
}
});
return changed;
},
/**
* Whenever the dimensions change of a video stream, this function is called
* to process these changes and possibly trigger an update to the video
* container elements.
*
* @param {Object} localVideoDimensions Object containing 'width' and 'height'
* properties grouped by stream name
* @param {Object} remoteVideoDimensions Object containing 'width' and 'height'
* properties grouped by stream name
*/
updateVideoDimensions: function(localVideoDimensions, remoteVideoDimensions) {
var localChanged = this._updateDimensionsCache("local", localVideoDimensions);
var remoteChanged = this._updateDimensionsCache("remote", remoteVideoDimensions);
if (localChanged || remoteChanged) {
this.updateVideoContainer();
}
},
/**
* Get the aspect ratio of a width/ height pair, which should be the dimensions
* of a stream. The returned object is an aspect ratio indexed by 1; the leading
* size has a value smaller than 1 and the slave size has a value of 1.
* this is exactly the same as notations like 4:3 and 16:9, which are merely
* human-readable forms of their fractional counterparts. 4:3 === 1:0.75 and
* 16:9 === 1:0.5625.
* So we're using the aspect ratios in their original form, because that's
* easier to do calculus with.
*
* Example:
* A stream with dimensions `{ width: 640, height: 480 }` yields an indexed
* aspect ratio of `{ width: 1, height: 0.75 }`. This means that the 'height'
* will determine the value of 'width' when the stream is stretched or shrunk
* to fit inside its container element at the maximum size.
*
* @param {Object} dimensions Object containing 'width' and 'height' properties
* @return {Object} Contains the indexed aspect ratio for 'width'
* and 'height' assigned to the corresponding
* properties.
*/
getAspectRatio: function(dimensions) {
if (dimensions.width === dimensions.height) {
return {width: 1, height: 1};
}
var denominator = Math.max(dimensions.width, dimensions.height);
return {
width: dimensions.width / denominator,
height: dimensions.height / denominator
};
},
/**
* Retrieve the dimensions of the active remote video stream. This assumes
* that if screens are being shared, the remote camera stream is hidden.
* Example output:
* {
* width: 680,
* height: 480,
* streamWidth: 640,
* streamHeight: 480,
* offsetX: 20,
* offsetY: 0
* }
*
* Note: This expects a class on the element that has the name "remote" or the
* same name as the possible video types (currently only "screen").
* Note: Once we support multiple remote video streams, this function will
* need to be updated.
*
* @param {string} videoType The video type according to the sdk, e.g. "camera" or
* "screen".
* @return {Object} contains the remote stream dimension properties of its
* container node, the stream itself and offset of the stream
* relative to its container node in pixels.
*/
getRemoteVideoDimensions: function(videoType) {
var remoteVideoDimensions;
if (videoType in this._videoDimensionsCache.remote) {
var node = this._getElement("." + (videoType === "camera" ? "remote" : videoType));
var width = node.offsetWidth;
// If the width > 0 then we record its real size by taking its aspect
// ratio in account. Due to the 'contain' fit-mode, the stream will be
// centered inside the video element.
// We'll need to deal with more than one remote video stream whenever
// that becomes something we need to support.
if (width) {
remoteVideoDimensions = {
width: width,
height: node.offsetHeight
};
var ratio = this._videoDimensionsCache.remote[videoType].aspectRatio;
// Leading axis is the side that has the smallest ratio.
var leadingAxis = Math.min(ratio.width, ratio.height) === ratio.width ?
"width" : "height";
var slaveAxis = leadingAxis === "height" ? "width" : "height";
// We need to work out if the leading axis of the video is full, by
// calculating the expected length of the leading axis based on the
// length of the slave axis and aspect ratio.
var leadingAxisFull = remoteVideoDimensions[slaveAxis] * ratio[leadingAxis] >
remoteVideoDimensions[leadingAxis];
if (leadingAxisFull) {
// If the leading axis is "full" then we need to adjust the slave axis.
var slaveAxisSize = remoteVideoDimensions[leadingAxis] / ratio[leadingAxis];
remoteVideoDimensions.streamWidth = leadingAxis === "width" ?
remoteVideoDimensions.width : slaveAxisSize;
remoteVideoDimensions.streamHeight = leadingAxis === "height" ?
remoteVideoDimensions.height : slaveAxisSize;
} else {
// If the leading axis is not "full" then we need to adjust it, based
// on the length of the leading axis.
var leadingAxisSize = remoteVideoDimensions[slaveAxis] * ratio[leadingAxis];
remoteVideoDimensions.streamWidth = leadingAxis === "height" ?
remoteVideoDimensions.width : leadingAxisSize;
remoteVideoDimensions.streamHeight = leadingAxis === "width" ?
remoteVideoDimensions.height : leadingAxisSize;
}
}
}
// Supply some sensible defaults for the remoteVideoDimensions if no remote
// stream is connected (yet).
if (!remoteVideoDimensions) {
node = this._getElement(".remote");
width = node.offsetWidth;
var height = node.offsetHeight;
remoteVideoDimensions = {
width: width,
height: height,
streamWidth: width,
streamHeight: height
};
}
// Calculate the size of each individual letter- or pillarbox for convenience.
remoteVideoDimensions.offsetX = remoteVideoDimensions.width -
remoteVideoDimensions.streamWidth;
if (remoteVideoDimensions.offsetX > 0) {
remoteVideoDimensions.offsetX /= 2;
}
remoteVideoDimensions.offsetY = remoteVideoDimensions.height -
remoteVideoDimensions.streamHeight;
if (remoteVideoDimensions.offsetY > 0) {
remoteVideoDimensions.offsetY /= 2;
}
return remoteVideoDimensions;
},
/**
* Used to update the video container whenever the orientation or size of the
* display area changes.
*
* Buffer the calls to this function to make sure we don't overflow the stack
* with update calls when many 'resize' event are fired, to prevent blocking
* the event loop.
*/
updateVideoContainer: function() {
if (this._bufferedUpdateVideo) {
rootObject.clearTimeout(this._bufferedUpdateVideo);
this._bufferedUpdateVideo = null;
}
this._bufferedUpdateVideo = rootObject.setTimeout(function() {
// Since this is being called from setTimeout, any exceptions thrown
// will propagate upwards into nothingness, unless we go out of our
// way to catch and log them explicitly, so...
try {
this._bufferedUpdateVideo = null;
var localStreamParent = this._getElement(".local .OT_publisher");
var remoteStreamParent = this._getElement(".remote .OT_subscriber");
var screenShareStreamParent = this._getElement(".screen .OT_subscriber");
if (localStreamParent) {
localStreamParent.style.width = "100%";
}
if (remoteStreamParent) {
remoteStreamParent.style.height = "100%";
}
if (screenShareStreamParent) {
screenShareStreamParent.style.height = "100%";
}
// Update the position and dimensions of the containers of local and
// remote video streams, if necessary. The consumer of this mixin
// should implement the actual updating mechanism.
Object.keys(this._videoDimensionsCache.local).forEach(
function (videoType) {
var ratio = this._videoDimensionsCache.local[videoType].aspectRatio;
if (videoType == "camera" && this.updateLocalCameraPosition) {
this.updateLocalCameraPosition(ratio);
}
}, this);
Object.keys(this._videoDimensionsCache.remote).forEach(
function (videoType) {
var ratio = this._videoDimensionsCache.remote[videoType].aspectRatio;
if (videoType == "camera" && this.updateRemoteCameraPosition) {
this.updateRemoteCameraPosition(ratio);
}
}, this);
} catch (ex) {
console.error("updateVideoContainer: _bufferedVideoUpdate exception:", ex);
}
}.bind(this), 0);
},
/**
* Returns the default configuration for publishing media on the sdk.
*
@ -576,15 +308,6 @@ loop.shared.mixins = (function() {
publishVideo: options.publishVideo,
showControls: false
};
},
/**
* Returns either the required DOMNode
*
* @param {String} className The name of the class to get the element for.
*/
_getElement: function(className) {
return this.getDOMNode().querySelector(className);
}
};

View File

@ -192,233 +192,30 @@ describe("loop.shared.mixins", function() {
});
describe("loop.shared.mixins.MediaSetupMixin", function() {
var view, TestComp, rootObject;
var localElement, remoteElement, screenShareElement;
var view;
beforeEach(function() {
TestComp = React.createClass({
var TestComp = React.createClass({
mixins: [loop.shared.mixins.MediaSetupMixin],
render: function() {
return React.DOM.div();
}
});
sandbox.useFakeTimers();
rootObject = {
events: {},
setTimeout: function(func, timeout) {
return setTimeout(func, timeout);
},
clearTimeout: function(timer) {
return clearTimeout(timer);
},
addEventListener: function(eventName, listener) {
this.events[eventName] = listener;
},
removeEventListener: function(eventName) {
delete this.events[eventName];
}
};
sharedMixins.setRootObject(rootObject);
view = TestUtils.renderIntoDocument(React.createElement(TestComp));
sandbox.stub(view, "getDOMNode").returns({
querySelector: function(classSelector) {
if (classSelector.indexOf("local") > -1) {
return localElement;
} else if (classSelector.indexOf("screen") > -1) {
return screenShareElement;
}
return remoteElement;
}
});
});
afterEach(function() {
localElement = null;
remoteElement = null;
screenShareElement = null;
});
describe("#getDefaultPublisherConfig", function() {
it("should provide a default publisher configuration", function() {
var defaultConfig = view.getDefaultPublisherConfig({publishVideo: true});
expect(defaultConfig.publishVideo).eql(true);
});
});
describe("#getRemoteVideoDimensions", function() {
var localVideoDimensions, remoteVideoDimensions;
beforeEach(function() {
localVideoDimensions = {
camera: {
width: 640,
height: 480
}
};
it("should throw if publishVideo is not given", function() {
expect(function() {
view.getDefaultPublisherConfig();
}).to.throw(/missing/);
});
it("should fetch the correct stream sizes for leading axis width and full",
function() {
remoteVideoDimensions = {
screen: {
width: 240,
height: 320
}
};
screenShareElement = {
offsetWidth: 480,
offsetHeight: 700
};
view.updateVideoDimensions(localVideoDimensions, remoteVideoDimensions);
var result = view.getRemoteVideoDimensions("screen");
expect(result.width).eql(screenShareElement.offsetWidth);
expect(result.height).eql(screenShareElement.offsetHeight);
expect(result.streamWidth).eql(screenShareElement.offsetWidth);
// The real height of the stream accounting for the aspect ratio.
expect(result.streamHeight).eql(640);
expect(result.offsetX).eql(0);
// The remote element height (700) minus the stream height (640) split in 2.
expect(result.offsetY).eql(30);
});
it("should fetch the correct stream sizes for leading axis width and not full",
function() {
remoteVideoDimensions = {
camera: {
width: 240,
height: 320
}
};
remoteElement = {
offsetWidth: 640,
offsetHeight: 480
};
view.updateVideoDimensions(localVideoDimensions, remoteVideoDimensions);
var result = view.getRemoteVideoDimensions("camera");
expect(result.width).eql(remoteElement.offsetWidth);
expect(result.height).eql(remoteElement.offsetHeight);
// Aspect ratio modified from the height.
expect(result.streamWidth).eql(360);
expect(result.streamHeight).eql(remoteElement.offsetHeight);
// The remote element width (640) minus the stream width (360) split in 2.
expect(result.offsetX).eql(140);
expect(result.offsetY).eql(0);
});
it("should fetch the correct stream sizes for leading axis height and full",
function() {
remoteVideoDimensions = {
screen: {
width: 320,
height: 240
}
};
screenShareElement = {
offsetWidth: 700,
offsetHeight: 480
};
view.updateVideoDimensions(localVideoDimensions, remoteVideoDimensions);
var result = view.getRemoteVideoDimensions("screen");
expect(result.width).eql(screenShareElement.offsetWidth);
expect(result.height).eql(screenShareElement.offsetHeight);
// The real width of the stream accounting for the aspect ratio.
expect(result.streamWidth).eql(640);
expect(result.streamHeight).eql(screenShareElement.offsetHeight);
// The remote element width (700) minus the stream width (640) split in 2.
expect(result.offsetX).eql(30);
expect(result.offsetY).eql(0);
});
it("should fetch the correct stream sizes for leading axis height and not full",
function() {
remoteVideoDimensions = {
camera: {
width: 320,
height: 240
}
};
remoteElement = {
offsetWidth: 480,
offsetHeight: 640
};
view.updateVideoDimensions(localVideoDimensions, remoteVideoDimensions);
var result = view.getRemoteVideoDimensions("camera");
expect(result.width).eql(remoteElement.offsetWidth);
expect(result.height).eql(remoteElement.offsetHeight);
expect(result.streamWidth).eql(remoteElement.offsetWidth);
// Aspect ratio modified from the width.
expect(result.streamHeight).eql(360);
expect(result.offsetX).eql(0);
// The remote element width (640) minus the stream width (360) split in 2.
expect(result.offsetY).eql(140);
});
});
describe("Events", function() {
describe("Video stream dimensions", function() {
var localVideoDimensions = {
camera: {
width: 640,
height: 480
}
};
var remoteVideoDimensions = {
camera: {
width: 420,
height: 138
}
};
it("should register video dimension updates correctly", function() {
view.updateVideoDimensions(localVideoDimensions, remoteVideoDimensions);
expect(view._videoDimensionsCache.local.camera.width)
.eql(localVideoDimensions.camera.width);
expect(view._videoDimensionsCache.local.camera.height)
.eql(localVideoDimensions.camera.height);
expect(view._videoDimensionsCache.local.camera.aspectRatio.width).eql(1);
expect(view._videoDimensionsCache.local.camera.aspectRatio.height).eql(0.75);
expect(view._videoDimensionsCache.remote.camera.width)
.eql(remoteVideoDimensions.camera.width);
expect(view._videoDimensionsCache.remote.camera.height)
.eql(remoteVideoDimensions.camera.height);
expect(view._videoDimensionsCache.remote.camera.aspectRatio.width).eql(1);
expect(view._videoDimensionsCache.remote.camera.aspectRatio.height)
.eql(0.32857142857142857);
});
it("should unregister video dimension updates correctly", function() {
view.updateVideoDimensions(localVideoDimensions, {});
expect("camera" in view._videoDimensionsCache.local).eql(true);
expect("camera" in view._videoDimensionsCache.remote).eql(false);
});
it("should not populate the cache on another component instance", function() {
view.updateVideoDimensions(localVideoDimensions, remoteVideoDimensions);
var view2 =
TestUtils.renderIntoDocument(React.createElement(TestComp));
expect(view2._videoDimensionsCache.local).to.be.empty;
expect(view2._videoDimensionsCache.remote).to.be.empty;
});
it("should return a set of defaults based on the options", function() {
expect(view.getDefaultPublisherConfig({
publishVideo: true
}).publishVideo).eql(true);
});
});
});

View File

@ -9,6 +9,8 @@ const TEST_URI = "data:text/html;charset=utf-8,Web Console HPKP invalid " +
"header test";
const SJS_URL = "https://example.com/browser/browser/devtools/webconsole/" +
"test/test_hpkp-invalid-headers.sjs";
const LEARN_MORE_URI = "https://developer.mozilla.org/docs/Web/Security/" +
"Public_Key_Pinning";
const NON_BUILTIN_ROOT_PREF = "security.cert_pinning.process_headers_from_" +
"non_builtin_roots";
@ -86,7 +88,7 @@ function* checkForMessage(curTest, hud) {
content.location = curTest.url;
yield waitForMessages({
let results = yield waitForMessages({
webconsole: hud,
messages: [
{
@ -94,7 +96,17 @@ function* checkForMessage(curTest, hud) {
text: curTest.text,
category: CATEGORY_SECURITY,
severity: SEVERITY_WARNING,
objects: true,
},
],
});
yield testClickOpenNewTab(hud, results);
}
function testClickOpenNewTab(hud, results) {
let warningNode = results[0].clickableElements[0];
ok(warningNode, "link element");
ok(warningNode.classList.contains("learn-more-link"), "link class name");
return simulateMessageLinkClick(warningNode, LEARN_MORE_URI);
}

View File

@ -46,6 +46,8 @@ const TRACKING_PROTECTION_LEARN_MORE = "https://developer.mozilla.org/Firefox/Pr
const INSECURE_PASSWORDS_LEARN_MORE = "https://developer.mozilla.org/docs/Security/InsecurePasswords";
const PUBLIC_KEY_PINS_LEARN_MORE = "https://developer.mozilla.org/docs/Web/Security/Public_Key_Pinning";
const STRICT_TRANSPORT_SECURITY_LEARN_MORE = "https://developer.mozilla.org/docs/Security/HTTP_Strict_Transport_Security";
const WEAK_SIGNATURE_ALGORITHM_LEARN_MORE = "https://developer.mozilla.org/docs/Security/Weak_Signature_Algorithm";
@ -1668,25 +1670,28 @@ WebConsoleFrame.prototype = {
{
let url;
switch (aScriptError.category) {
case "Insecure Password Field":
url = INSECURE_PASSWORDS_LEARN_MORE;
break;
case "Mixed Content Message":
case "Mixed Content Blocker":
url = MIXED_CONTENT_LEARN_MORE;
break;
case "Invalid HSTS Headers":
url = STRICT_TRANSPORT_SECURITY_LEARN_MORE;
break;
case "SHA-1 Signature":
url = WEAK_SIGNATURE_ALGORITHM_LEARN_MORE;
break;
case "Tracking Protection":
url = TRACKING_PROTECTION_LEARN_MORE;
break;
default:
// Unknown category. Return without adding more info node.
return;
case "Insecure Password Field":
url = INSECURE_PASSWORDS_LEARN_MORE;
break;
case "Mixed Content Message":
case "Mixed Content Blocker":
url = MIXED_CONTENT_LEARN_MORE;
break;
case "Invalid HPKP Headers":
url = PUBLIC_KEY_PINS_LEARN_MORE;
break;
case "Invalid HSTS Headers":
url = STRICT_TRANSPORT_SECURITY_LEARN_MORE;
break;
case "SHA-1 Signature":
url = WEAK_SIGNATURE_ALGORITHM_LEARN_MORE;
break;
case "Tracking Protection":
url = TRACKING_PROTECTION_LEARN_MORE;
break;
default:
// Unknown category. Return without adding more info node.
return;
}
this.addLearnMoreWarningNode(aNode, url);

View File

@ -21,10 +21,26 @@ Components.utils.import('resource://gre/modules/Promise.jsm');
Components.utils.import('resource://gre/modules/XPCOMUtils.jsm');
Components.utils.import('resource://gre/modules/NetUtil.jsm');
function FileLoader(swfUrl, baseUrl, callback) {
function FileLoaderSession(sessionId) {
this.sessionId = sessionId;
this.xhr = null;
}
FileLoaderSession.prototype = {
abort() {
if (this.xhr) {
this.xhr.abort();
this.xhr = null;
}
}
};
function FileLoader(swfUrl, baseUrl, refererUrl, callback) {
this.swfUrl = swfUrl;
this.baseUrl = baseUrl;
this.refererUrl = refererUrl;
this.callback = callback;
this.activeSessions = Object.create(null);
this.crossdomainRequestsCache = Object.create(null);
}
@ -50,18 +66,26 @@ FileLoader.prototype = {
var method = data.method || "GET";
var mimeType = data.mimeType;
var postData = data.postData || null;
var sendReferer = canSendReferer(swfUrl, this.refererUrl);
var session = new FileLoaderSession(sessionId);
this.activeSessions[sessionId] = session;
var self = this;
var performXHR = function () {
var xhr = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Components.interfaces.nsIXMLHttpRequest);
// Load has been aborted before we reached this point.
if (!self.activeSessions[sessionId]) {
return;
}
var xhr = session.xhr = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Components.interfaces.nsIXMLHttpRequest);
xhr.open(method, url, true);
xhr.responseType = "moz-chunked-arraybuffer";
if (baseUrl) {
if (sendReferer) {
// Setting the referer uri, some site doing checks if swf is embedded
// on the original page.
xhr.setRequestHeader("Referer", baseUrl);
xhr.setRequestHeader("Referer", self.refererUrl);
}
// TODO apply range request headers if limit is specified
@ -69,16 +93,23 @@ FileLoader.prototype = {
var lastPosition = 0;
xhr.onprogress = function (e) {
var position = e.loaded;
var total = e.total;
var data = new Uint8Array(xhr.response);
// The event's `loaded` and `total` properties are sometimes lower than the actual
// number of loaded bytes. In that case, increase them to that value.
position = Math.max(position, data.byteLength);
total = Math.max(total, data.byteLength);
notifyLoadFileListener({callback:"loadFile", sessionId: sessionId,
topic: "progress", array: data, loaded: position, total: e.total});
topic: "progress", array: data, loaded: position, total: total});
lastPosition = position;
if (limit && e.total >= limit) {
if (limit && total >= limit) {
xhr.abort();
}
};
xhr.onreadystatechange = function(event) {
if (xhr.readyState === 4) {
delete self.activeSessions[sessionId];
if (xhr.status !== 200 && xhr.status !== 0) {
notifyLoadFileListener({callback:"loadFile", sessionId: sessionId, topic: "error", error: xhr.statusText});
}
@ -95,9 +126,21 @@ FileLoader.prototype = {
performXHR();
}, function (reason) {
log("data access is prohibited to " + url + " from " + baseUrl);
notifyLoadFileListener({callback:"loadFile", sessionId: sessionId, topic: "error",
error: "only original swf file or file from the same origin loading supported"});
delete self.activeSessions[sessionId];
notifyLoadFileListener({
callback: "loadFile", sessionId: sessionId, topic: "error",
error: "only original swf file or file from the same origin loading supported (XDOMAIN)"
});
});
},
abort: function(sessionId) {
var session = this.activeSessions[sessionId];
if (!session) {
log("Warning: trying to abort invalid session " + sessionId);
return;
}
session.abort();
delete this.activeSessions[sessionId];
}
};
@ -129,6 +172,25 @@ function disableXHRRedirect(xhr) {
xhr.channel.notificationCallbacks = listener;
}
function canSendReferer(url, refererUrl) {
if (!refererUrl) {
return false;
}
// Allow sending HTTPS referer only to HTTPS.
var parsedUrl, parsedRefererUrl;
try {
parsedRefererUrl = NetUtil.newURI(refererUrl);
} catch (ex) { /* skipping invalid urls */ }
if (!parsedRefererUrl ||
parsedRefererUrl.scheme.toLowerCase() !== 'https') {
return true;
}
try {
parsedUrl = NetUtil.newURI(url);
} catch (ex) { /* skipping invalid urls */ }
return !!parsedUrl && parsedUrl.scheme.toLowerCase() === 'https';
}
function canDownloadFile(url, checkPolicyFile, swfUrl, cache) {
// TODO flash cross-origin request
if (url === swfUrl) {

View File

@ -0,0 +1,258 @@
/*
* Copyright 2015 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var EXPORTED_SYMBOLS = ['LocalConnectionService'];
Components.utils.import('resource://gre/modules/NetUtil.jsm');
Components.utils.import('resource://gre/modules/Services.jsm');
const localConnectionsRegistry = Object.create(null);
function isConnectionNameValid(connectionName) {
return typeof connectionName === 'string' &&
(connectionName[0] === '_' || connectionName.split(':').length === 2);
}
/**
* Creates a trusted qualified connection name from an already qualified name and a swfUrl.
*
* While connection names are already qualified at this point, the qualification happens in
* untrusted code. To ensure that the name is correctly qualified, this function compares the
* qualification domain with the current SWF's URL and substitutes that URL's domain if
* required. A warning is logged in that case.
*/
function _getQualifiedConnectionName(connectionName, swfUrl) {
// Already syntactically invalid connection names mustn't get here.
if (!isConnectionNameValid(connectionName)) {
// TODO: add telemetry
throw new Error('Syntactically invalid local-connection name encountered', connectionName,
swfUrl);
}
var [domain, name] = connectionName.split(':');
var parsedURL = NetUtil.newURI(swfUrl);
if (domain !== parsedURL.host) {
// TODO: add telemetry
log('Warning: invalid local-connection name qualification found: ' + connectionName);
return parsedURL.host + ':' + swfUrl;
}
return connectionName;
}
function _getLocalConnection(connectionName) {
// Treat invalid connection names as non-existent. This can only happen if player code
// misbehaves, though.
if (!isConnectionNameValid(connectionName)) {
// TODO: add telemetry
return null;
}
var connection = localConnectionsRegistry[connectionName];
if (connection && Components.utils.isDeadWrapper(connection.callback)) {
delete localConnectionsRegistry[connectionName];
return null;
}
return localConnectionsRegistry[connectionName];
}
function LocalConnectionService(content, environment) {
var traceLocalConnection = getBoolPref('shumway.localConnection.trace', false);
var api = {
createLocalConnection: function (connectionName, callback) {
connectionName = connectionName + '';
traceLocalConnection && content.console.log(`Creating local connection "${connectionName}" ` +
`for SWF with URL ${environment.swfUrl}`);
if (!isConnectionNameValid(connectionName)) {
// TODO: add telemetry
traceLocalConnection && content.console.warn(`Invalid localConnection name `);
return -1; // LocalConnectionConnectResult.InvalidName
}
if (typeof callback !== 'function') {
// TODO: add telemetry
traceLocalConnection && content.console.warn(`Invalid callback for localConnection`);
return -3; // LocalConnectionConnectResult.InvalidCallback
}
connectionName = _getQualifiedConnectionName(connectionName, environment.swfUrl);
if (_getLocalConnection(connectionName)) {
traceLocalConnection && content.console.log(`localConnection ` +
`name "${connectionName}" already taken`);
return -2; // LocalConnectionConnectResult.AlreadyTaken
}
var parsedURL = NetUtil.newURI(environment.swfUrl);
var connection = {
callback,
domain: parsedURL.host,
environment: environment,
secure: parsedURL.protocol === 'https:',
allowedSecureDomains: Object.create(null),
allowedInsecureDomains: Object.create(null)
};
localConnectionsRegistry[connectionName] = connection;
return 0; // LocalConnectionConnectResult.Success
},
hasLocalConnection: function (connectionName) {
connectionName = _getQualifiedConnectionName(connectionName + '', environment.swfUrl);
var result = !!_getLocalConnection(connectionName);
traceLocalConnection && content.console.log(`hasLocalConnection "${connectionName}"? ` +
result);
return result;
},
closeLocalConnection: function (connectionName) {
connectionName = _getQualifiedConnectionName(connectionName + '', environment.swfUrl);
traceLocalConnection && content.console.log(`Closing local connection "${connectionName}" ` +
`for SWF with URL ${environment.swfUrl}`);
var connection = _getLocalConnection(connectionName);
if (!connection) {
traceLocalConnection && content.console.log(`localConnection "${connectionName}" not ` +
`connected`);
return -1; // LocalConnectionCloseResult.NotConnected
} else if (connection.environment !== environment) {
// Attempts to close connections from a SWF instance that didn't create them shouldn't
// happen. If they do, we treat them as if the connection didn't exist.
traceLocalConnection && content.console.warn(`Ignored attempt to close localConnection ` +
`"${connectionName}" from SWF instance that ` +
`didn't create it`);
return -1; // LocalConnectionCloseResult.NotConnected
}
delete localConnectionsRegistry[connectionName];
return 0; // LocalConnectionCloseResult.Success
},
sendLocalConnectionMessage: function (connectionName, methodName, argsBuffer, sender,
senderDomain, senderIsSecure) {
connectionName = connectionName + '';
methodName = methodName + '';
senderDomain = senderDomain + '';
senderIsSecure = !!senderIsSecure;
// TODO: sanitize argsBuffer argument. Ask bholley how to do so.
traceLocalConnection && content.console.log(`sending localConnection message ` +
`"${methodName}" to "${connectionName}"`);
// Since we don't currently trust the sender information passed in here, we use the
// currently running SWF's URL instead.
var parsedURL = NetUtil.newURI(environment.swfUrl);
var parsedURLIsSecure = parsedURL.protocol === 'https:';
if (parsedURL.host !== senderDomain || parsedURLIsSecure !== senderIsSecure) {
traceLocalConnection && content.console.warn(`sending localConnection message ` +
`"${methodName}" to "${connectionName}"`);
}
senderDomain = parsedURL.host;
senderIsSecure = parsedURLIsSecure;
var connection = _getLocalConnection(connectionName);
if (!connection) {
traceLocalConnection && content.console.log(`localConnection "${connectionName}" not ` +
`connected`);
return;
}
try {
var allowed = false;
if (connection.secure) {
// If the receiver is secure, the sender has to be, too, or it has to be whitelisted
// with allowInsecureDomain.
if (senderIsSecure) {
if (senderDomain === connection.domain ||
senderDomain in connection.allowedSecureDomains ||
'*' in connection.allowedSecureDomains) {
allowed = true;
}
} else {
if (senderDomain in connection.allowedInsecureDomains ||
'*' in connection.allowedInsecureDomains) {
allowed = true;
}
}
} else {
// For non-secure connections, allowedSecureDomains is expected to contain all allowed
// domains, secure on non-secure, so we don't have to check both.
if (senderDomain === connection.domain ||
senderDomain in connection.allowedSecureDomains ||
'*' in connection.allowedSecureDomains) {
allowed = true;
}
}
if (!allowed) {
traceLocalConnection && content.console.warn(`LocalConnection message rejected: domain ` +
`${senderDomain} not allowed.`);
return {
name: 'SecurityError',
$Bgmessage: "The current security context does not allow this operation.",
_errorID: 3315
};
}
var callback = connection.callback;
var clonedArgs = Components.utils.cloneInto(argsBuffer, callback);
callback(methodName, clonedArgs);
} catch (e) {
// TODO: add telemetry
content.console.warn('Unexpected error encountered while sending LocalConnection message.');
}
},
allowDomainsForLocalConnection: function (connectionName, domains, secure) {
connectionName = _getQualifiedConnectionName(connectionName + '', environment.swfUrl);
secure = !!secure;
var connection = _getLocalConnection(connectionName);
if (!connection) {
return;
}
try {
domains = Components.utils.cloneInto(domains, connection);
} catch (e) {
log('error in allowDomainsForLocalConnection: ' + e);
return;
}
traceLocalConnection && content.console.log(`allowing ${secure ? '' : 'in'}secure domains ` +
`[${domains}] for localConnection ` +
`"${connectionName}"`);
function validateDomain(domain) {
if (typeof domain !== 'string') {
return false;
}
if (domain === '*') {
return true;
}
try {
var uri = NetUtil.newURI('http://' + domain);
return uri.host === domain;
} catch (e) {
return false;
}
}
if (!Array.isArray(domains) || !domains.every(validateDomain)) {
traceLocalConnection && content.console.warn(`Invalid domains rejected`);
return;
}
var allowedDomains = secure ?
connection.allowedSecureDomains :
connection.allowedInsecureDomains;
domains.forEach(domain => allowedDomains[domain] = true);
}
};
// Don't return `this` even though this function is treated as a ctor. Makes cloning into the
// content compartment an internal operation the client code doesn't have to worry about.
return Components.utils.cloneInto(api, content, {cloneFunctions:true});
}
function getBoolPref(pref, def) {
try {
return Services.prefs.getBoolPref(pref);
} catch (ex) {
return def;
}
}

View File

@ -16,15 +16,17 @@
var EXPORTED_SYMBOLS = ['ShumwayCom'];
Components.utils.import('resource://gre/modules/XPCOMUtils.jsm');
Components.utils.import('resource://gre/modules/Services.jsm');
Components.utils.import('resource://gre/modules/NetUtil.jsm');
Components.utils.import('resource://gre/modules/Promise.jsm');
Components.utils.import('resource://gre/modules/Services.jsm');
Components.utils.import('resource://gre/modules/XPCOMUtils.jsm');
Components.utils.import('chrome://shumway/content/SpecialInflate.jsm');
Components.utils.import('chrome://shumway/content/SpecialStorage.jsm');
Components.utils.import('chrome://shumway/content/RtmpUtils.jsm');
Components.utils.import('chrome://shumway/content/ExternalInterface.jsm');
Components.utils.import('chrome://shumway/content/FileLoader.jsm');
Components.utils.import('chrome://shumway/content/LocalConnection.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'ShumwayTelemetry',
'resource://shumway/ShumwayTelemetry.jsm');
@ -75,6 +77,9 @@ function sanitizeTelemetryArgs(args) {
case 'feature':
request.featureType = args.feature | 0;
break;
case 'loadResource':
request.resultType = args.resultType | 0;
break;
case 'error':
request.errorType = args.error | 0;
break;
@ -89,6 +94,7 @@ function sanitizeLoadFileArgs(args) {
sessionId: +args.sessionId,
limit: +args.limit || 0,
mimeType: String(args.mimeType || ''),
method: (args.method + '') || 'GET',
postData: args.postData || null
};
}
@ -112,109 +118,190 @@ function sanitizeExternalComArgs(args) {
return request;
}
var cloneIntoFromContent = (function () {
// waiveXrays are used due to bug 1150771, checking if we are affected
// TODO remove workaround after Firefox 40 is released (2015-08-11)
let sandbox1 = new Components.utils.Sandbox(null);
let sandbox2 = new Components.utils.Sandbox(null);
let arg = Components.utils.evalInSandbox('({buf: new ArrayBuffer(2)})', sandbox1);
let clonedArg = Components.utils.cloneInto(arg, sandbox2);
if (!Components.utils.waiveXrays(clonedArg).buf) {
return function (obj, contentSandbox) {
return Components.utils.cloneInto(
Components.utils.waiveXrays(obj), contentSandbox);
};
}
return function (obj, contentSandbox) {
return Components.utils.cloneInto(obj, contentSandbox);
};
})();
var ShumwayEnvironment = {
DEBUG: 'debug',
DEVELOPMENT: 'dev',
RELEASE: 'release',
TEST: 'test'
};
var ShumwayCom = {
environment: getCharPref('shumway.environment', 'dev'),
createAdapter: function (content, callbacks, hooks) {
// Exposing ShumwayCom object/adapter to the unprivileged content -- setting
// up Xray wrappers.
var wrapped = {
enableDebug: function enableDebug() {
environment: ShumwayCom.environment,
enableDebug: function () {
callbacks.enableDebug()
},
setFullscreen: function setFullscreen(value) {
value = !!value;
callbacks.sendMessage('setFullscreen', value, false);
},
fallback: function fallback() {
fallback: function () {
callbacks.sendMessage('fallback', null, false);
},
getSettings: function getSettings() {
getSettings: function () {
return Components.utils.cloneInto(
callbacks.sendMessage('getSettings', null, true), content);
},
getPluginParams: function getPluginParams() {
getPluginParams: function () {
return Components.utils.cloneInto(
callbacks.sendMessage('getPluginParams', null, true), content);
},
reportIssue: function reportIssue() {
reportIssue: function () {
callbacks.sendMessage('reportIssue', null, false);
},
reportTelemetry: function reportTelemetry(args) {
reportTelemetry: function (args) {
var request = sanitizeTelemetryArgs(args);
callbacks.sendMessage('reportTelemetry', request, false);
},
userInput: function userInput() {
callbacks.sendMessage('userInput', null, true);
setupGfxComBridge: function (gfxWindow) {
// Creates ShumwayCom adapter for the gfx iframe exposing only subset
// of the privileged function. Removing Xrays to setup the ShumwayCom
// property and for usage as a sandbox for cloneInto operations.
var gfxContent = gfxWindow.contentWindow.wrappedJSObject;
ShumwayCom.createGfxAdapter(gfxContent, callbacks, hooks);
setupUserInput(gfxWindow.contentWindow, callbacks);
},
setupComBridge: function setupComBridge(playerWindow) {
// postSyncMessage helper function to relay messages from the secondary
// window to the primary one.
function postSyncMessage(msg) {
if (onSyncMessageCallback) {
// the msg came from other content window
// waiveXrays are used due to bug 1150771.
var reclonedMsg = Components.utils.cloneInto(Components.utils.waiveXrays(msg), content);
var result = onSyncMessageCallback(reclonedMsg);
// the result will be sent later to other content window
var waivedResult = Components.utils.waiveXrays(result);
return waivedResult;
}
}
// Creates secondary ShumwayCom adapter.
setupPlayerComBridge: function (playerWindow) {
// Creates ShumwayCom adapter for the player iframe exposing only subset
// of the privileged function. Removing Xrays to setup the ShumwayCom
// property and for usage as a sandbox for cloneInto operations.
var playerContent = playerWindow.contentWindow.wrappedJSObject;
ShumwayCom.createPlayerAdapter(playerContent, postSyncMessage, callbacks, hooks);
},
setSyncMessageCallback: function (callback) {
if (callback !== null && typeof callback !== 'function') {
return;
}
onSyncMessageCallback = callback;
ShumwayCom.createPlayerAdapter(playerContent, callbacks, hooks);
}
};
var onSyncMessageCallback = null;
var shumwayComAdapter = Components.utils.cloneInto(wrapped, content, {cloneFunctions:true});
content.ShumwayCom = shumwayComAdapter;
},
createPlayerAdapter: function (content, postSyncMessage, callbacks, hooks) {
createGfxAdapter: function (content, callbacks, hooks) {
// Exposing ShumwayCom object/adapter to the unprivileged content -- setting
// up Xray wrappers.
var wrapped = {
externalCom: function externalCom(args) {
environment: ShumwayCom.environment,
setFullscreen: function (value) {
value = !!value;
callbacks.sendMessage('setFullscreen', value, false);
},
reportTelemetry: function (args) {
var request = sanitizeTelemetryArgs(args);
callbacks.sendMessage('reportTelemetry', request, false);
},
postAsyncMessage: function (msg) {
if (hooks.onPlayerAsyncMessageCallback) {
hooks.onPlayerAsyncMessageCallback(msg);
}
},
setSyncMessageCallback: function (callback) {
if (typeof callback !== 'function') {
log('error: attempt to set non-callable as callback in setSyncMessageCallback');
return;
}
hooks.onGfxSyncMessageCallback = function (msg, sandbox) {
var reclonedMsg = cloneIntoFromContent(msg, content);
var result = callback(reclonedMsg);
return cloneIntoFromContent(result, sandbox);
};
},
setAsyncMessageCallback: function (callback) {
if (typeof callback !== 'function') {
log('error: attempt to set non-callable as callback in setAsyncMessageCallback');
return;
}
hooks.onGfxAsyncMessageCallback = function (msg) {
var reclonedMsg = cloneIntoFromContent(msg, content);
callback(reclonedMsg);
};
}
};
if (ShumwayCom.environment === ShumwayEnvironment.TEST) {
wrapped.processFrame = function () {
callbacks.sendMessage('processFrame');
};
wrapped.processFSCommand = function (command, args) {
callbacks.sendMessage('processFSCommand', command, args);
};
wrapped.setScreenShotCallback = function (callback) {
callbacks.sendMessage('setScreenShotCallback', callback);
};
}
var shumwayComAdapter = Components.utils.cloneInto(wrapped, content, {cloneFunctions:true});
content.ShumwayCom = shumwayComAdapter;
},
createPlayerAdapter: function (content, callbacks, hooks) {
// Exposing ShumwayCom object/adapter to the unprivileged content -- setting
// up Xray wrappers.
var wrapped = {
environment: ShumwayCom.environment,
externalCom: function (args) {
var request = sanitizeExternalComArgs(args);
var result = String(callbacks.sendMessage('externalCom', request, true));
return result;
},
loadFile: function loadFile(args) {
loadFile: function (args) {
var request = sanitizeLoadFileArgs(args);
callbacks.sendMessage('loadFile', request, false);
},
reportTelemetry: function reportTelemetry(args) {
abortLoad: function (sessionId) {
sessionId = sessionId|0;
callbacks.sendMessage('abortLoad', sessionId, false);
},
reportTelemetry: function (args) {
var request = sanitizeTelemetryArgs(args);
callbacks.sendMessage('reportTelemetry', request, false);
},
setClipboard: function setClipboard(args) {
setClipboard: function (args) {
if (typeof args !== 'string') {
return; // ignore non-string argument
}
callbacks.sendMessage('setClipboard', args, false);
},
navigateTo: function navigateTo(args) {
navigateTo: function (args) {
var request = {
url: String(args.url || ''),
target: String(args.target || '')
@ -222,7 +309,7 @@ var ShumwayCom = {
callbacks.sendMessage('navigateTo', request, false);
},
loadSystemResource: function loadSystemResource(id) {
loadSystemResource: function (id) {
loadShumwaySystemResource(id).then(function (data) {
if (onSystemResourceCallback) {
onSystemResourceCallback(id, Components.utils.cloneInto(data, content));
@ -230,9 +317,29 @@ var ShumwayCom = {
});
},
postSyncMessage: function (msg) {
var result = postSyncMessage(msg);
return Components.utils.cloneInto(result, content)
sendSyncMessage: function (msg) {
var result;
if (hooks.onGfxSyncMessageCallback) {
result = hooks.onGfxSyncMessageCallback(msg, content);
}
return result;
},
postAsyncMessage: function (msg) {
if (hooks.onGfxAsyncMessageCallback) {
hooks.onGfxAsyncMessageCallback(msg);
}
},
setAsyncMessageCallback: function (callback) {
if (typeof callback !== 'function') {
log('error: attempt to set non-callable as callback in setAsyncMessageCallback');
return;
}
hooks.onPlayerAsyncMessageCallback = function (msg) {
var reclonedMsg = cloneIntoFromContent(msg, content);
callback(reclonedMsg);
};
},
createSpecialStorage: function () {
@ -267,6 +374,14 @@ var ShumwayCom = {
return;
}
onSystemResourceCallback = callback;
},
getLocalConnectionService: function() {
if (!wrappedLocalConnectionService) {
wrappedLocalConnectionService = new LocalConnectionService(content,
callbacks.getEnvironment());
}
return wrappedLocalConnectionService;
}
};
@ -289,10 +404,18 @@ var ShumwayCom = {
};
}
if (ShumwayCom.environment === ShumwayEnvironment.TEST) {
wrapped.print = function(msg) {
callbacks.sendMessage('print', msg);
}
}
var onSystemResourceCallback = null;
var onExternalCallback = null;
var onLoadFileCallback = null;
var wrappedLocalConnectionService = null;
hooks.onLoadFileCallback = function (arg) {
if (onLoadFileCallback) {
onLoadFileCallback(Components.utils.cloneInto(arg, content));
@ -346,6 +469,18 @@ function loadShumwaySystemResource(id) {
return deferred.promise;
}
function setupUserInput(contentWindow, callbacks) {
function notifyUserInput() {
callbacks.sendMessage('userInput', null, true);
}
// Ignoring the untrusted events by providing the 4th argument for addEventListener.
contentWindow.document.addEventListener('mousedown', notifyUserInput, true, false);
contentWindow.document.addEventListener('mouseup', notifyUserInput, true, false);
contentWindow.document.addEventListener('keydown', notifyUserInput, true, false);
contentWindow.document.addEventListener('keyup', notifyUserInput, true, false);
}
// All the privileged actions.
function ShumwayChromeActions(startupInfo, window, document) {
this.url = startupInfo.url;
@ -355,6 +490,7 @@ function ShumwayChromeActions(startupInfo, window, document) {
this.isOverlay = startupInfo.isOverlay;
this.embedTag = startupInfo.embedTag;
this.isPausedAtStart = startupInfo.isPausedAtStart;
this.initStartTime = startupInfo.initStartTime;
this.window = window;
this.document = document;
this.allowScriptAccess = startupInfo.allowScriptAccess;
@ -365,9 +501,8 @@ function ShumwayChromeActions(startupInfo, window, document) {
errors: []
};
this.fileLoader = new FileLoader(startupInfo.url, startupInfo.baseUrl, function (args) {
this.onLoadFileCallback(args);
}.bind(this));
this.fileLoader = new FileLoader(startupInfo.url, startupInfo.baseUrl, startupInfo.refererUrl,
function (args) { this.onLoadFileCallback(args); }.bind(this));
this.onLoadFileCallback = null;
this.externalInterface = null;
@ -400,8 +535,7 @@ ShumwayChromeActions.prototype = {
playerSettings: {
turboMode: getBoolPref('shumway.turboMode', false),
hud: getBoolPref('shumway.hud', false),
forceHidpi: getBoolPref('shumway.force_hidpi', false),
env: getCharPref('shumway.environment', 'dev')
forceHidpi: getBoolPref('shumway.force_hidpi', false)
}
}
},
@ -414,6 +548,7 @@ ShumwayChromeActions.prototype = {
objectParams: this.objectParams,
isOverlay: this.isOverlay,
isPausedAtStart: this.isPausedAtStart,
initStartTime: this.initStartTime,
isDebuggerEnabled: getBoolPref('shumway.debug.enabled', false)
};
},
@ -422,6 +557,10 @@ ShumwayChromeActions.prototype = {
this.fileLoader.load(data);
},
abortLoad: function abortLoad(sessionId) {
this.fileLoader.abort(sessionId);
},
navigateTo: function (data) {
// Our restrictions are a little bit different from Flash's: let's enable
// only http(s) and only when script execution is allowed.
@ -451,20 +590,11 @@ ShumwayChromeActions.prototype = {
},
userInput: function() {
var win = this.window;
var winUtils = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindowUtils);
if (winUtils.isHandlingUserInput) {
this.lastUserInput = Date.now();
}
// Recording time of last user input for isUserInputInProgress below.
this.lastUserInput = Date.now();
},
isUserInputInProgress: function () {
// TODO userInput does not work for OOP
if (!getBoolPref('shumway.userInputSecurity', true)) {
return true;
}
// We don't trust our Shumway non-privileged code just yet to verify the
// user input -- using userInput function above to track that.
if ((Date.now() - this.lastUserInput) > MAX_USER_INPUT_TIMEOUT) {
@ -515,6 +645,13 @@ ShumwayChromeActions.prototype = {
ShumwayTelemetry.onFeature(featureType);
}
break;
case 'loadResource':
var resultType = request.resultType;
var MIN_RESULT_TYPE = 0, MAX_RESULT_TYPE = 10;
if (resultType >= MIN_RESULT_TYPE && resultType <= MAX_RESULT_TYPE) {
ShumwayTelemetry.onLoadResource(resultType);
}
break;
case 'error':
var errorType = request.errorType;
var MIN_ERROR_TYPE = 0, MAX_ERROR_TYPE = 2;
@ -567,6 +704,35 @@ ShumwayChromeActions.prototype = {
}
return this.externalInterface.processAction(data);
},
postMessage: function (type, data) {
var embedTag = this.embedTag;
var event = embedTag.ownerDocument.createEvent('CustomEvent');
var detail = Components.utils.cloneInto({ type: type, data: data }, embedTag.ownerDocument.wrappedJSObject);
event.initCustomEvent('message', false, false, detail);
embedTag.dispatchEvent(event);
},
processFrame: function () {
this.postMessage('processFrame');
},
processFSCommand: function (command, data) {
this.postMessage('processFSCommand', { command: command, data: data });
},
print: function (msg) {
this.postMessage('print', msg);
},
setScreenShotCallback: function (callback) {
var embedTag = this.embedTag;
Components.utils.exportFunction(function () {
// `callback` can be wrapped in a CPOW and thus cause a slow synchronous cross-process operation.
var result = callback();
return Components.utils.cloneInto(result, embedTag.ownerDocument);
}, embedTag.wrappedJSObject, {defineAs: 'getCanvasData'});
}
};

View File

@ -0,0 +1,186 @@
/*
* Copyright 2015 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var EXPORTED_SYMBOLS = ['getStartupInfo', 'parseQueryString', 'isContentWindowPrivate'];
Components.utils.import('resource://gre/modules/XPCOMUtils.jsm');
Components.utils.import('resource://gre/modules/Services.jsm');
Components.utils.import('chrome://shumway/content/ShumwayCom.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'PrivateBrowsingUtils',
'resource://gre/modules/PrivateBrowsingUtils.jsm');
function flashUnescape(s) {
return decodeURIComponent(s.split('+').join(' '));
}
function parseQueryString(qs) {
if (!qs)
return {};
if (qs.charAt(0) == '?')
qs = qs.slice(1);
var values = qs.split('&');
var obj = {};
for (var i = 0; i < values.length; i++) {
var pair = values[i], j = pair.indexOf('=');
if (j < 0) {
continue; // skipping invalid values
}
var key = pair.substring(0, j), value = pair.substring(j + 1);
obj[flashUnescape(key)] = flashUnescape(value);
}
return obj;
}
function isContentWindowPrivate(win) {
if (!('isContentWindowPrivate' in PrivateBrowsingUtils)) {
return PrivateBrowsingUtils.isWindowPrivate(win);
}
return PrivateBrowsingUtils.isContentWindowPrivate(win);
}
function isStandardEmbedWrapper(embedElement) {
try {
if (embedElement.tagName !== 'EMBED') {
return false;
}
var swfUrl = embedElement.src;
var document = embedElement.ownerDocument;
var docUrl = document.location.href;
if (swfUrl !== docUrl) {
return false; // document URL shall match embed src
}
if (document.body.children.length !== 1 ||
document.body.firstChild !== embedElement) {
return false; // not the only child
}
if (document.defaultView.top !== document.defaultView) {
return false; // not a top window
}
// Looks like a standard wrapper
return true;
} catch (e) {
// Declare that is not a standard fullscreen plugin wrapper for any error
return false;
}
}
function isScriptAllowed(allowScriptAccessParameter, url, pageUrl) {
if (!allowScriptAccessParameter) {
allowScriptAccessParameter = 'sameDomain';
}
var allowScriptAccess = false;
switch (allowScriptAccessParameter.toLowerCase()) { // ignoring case here
case 'always':
allowScriptAccess = true;
break;
case 'never':
allowScriptAccess = false;
break;
default: // 'samedomain'
if (!pageUrl)
break;
try {
// checking if page is in same domain (? same protocol and port)
allowScriptAccess =
Services.io.newURI('/', null, Services.io.newURI(pageUrl, null, null)).spec ==
Services.io.newURI('/', null, Services.io.newURI(url, null, null)).spec;
} catch (ex) {}
break;
}
return allowScriptAccess;
}
function getStartupInfo(element) {
var initStartTime = Date.now();
var baseUrl;
var pageUrl;
var isOverlay = false;
var objectParams = {};
// Getting absolute URL from the EMBED tag
var url = element.srcURI && element.srcURI.spec;
pageUrl = element.ownerDocument.location.href; // proper page url?
var tagName = element.nodeName;
if (tagName == 'EMBED') {
for (var i = 0; i < element.attributes.length; ++i) {
var paramName = element.attributes[i].localName.toLowerCase();
objectParams[paramName] = element.attributes[i].value;
}
} else {
for (var i = 0; i < element.childNodes.length; ++i) {
var paramElement = element.childNodes[i];
if (paramElement.nodeType != 1 ||
paramElement.nodeName != 'PARAM') {
continue;
}
var paramName = paramElement.getAttribute('name').toLowerCase();
objectParams[paramName] = paramElement.getAttribute('value');
}
}
baseUrl = pageUrl;
if (objectParams.base) {
try {
// Verifying base URL, passed in object parameters. It shall be okay to
// ignore bad/corrupted base.
var parsedPageUrl = Services.io.newURI(pageUrl, null, null);
baseUrl = Services.io.newURI(objectParams.base, null, parsedPageUrl).spec;
} catch (e) { /* it's okay to ignore any exception */ }
}
var movieParams = {};
if (objectParams.flashvars) {
movieParams = parseQueryString(objectParams.flashvars);
}
var queryStringMatch = url && /\?([^#]+)/.exec(url);
if (queryStringMatch) {
var queryStringParams = parseQueryString(queryStringMatch[1]);
for (var i in queryStringParams) {
if (!(i in movieParams)) {
movieParams[i] = queryStringParams[i];
}
}
}
var allowScriptAccess = !!url &&
isScriptAllowed(objectParams.allowscriptaccess, url, pageUrl);
var isFullscreenSwf = isStandardEmbedWrapper(element);
var document = element.ownerDocument;
var window = document.defaultView;
var startupInfo = {};
startupInfo.window = window;
startupInfo.url = url;
startupInfo.privateBrowsing = isContentWindowPrivate(window);
startupInfo.objectParams = objectParams;
startupInfo.movieParams = movieParams;
startupInfo.baseUrl = baseUrl || url;
startupInfo.isOverlay = isOverlay;
startupInfo.refererUrl = !isFullscreenSwf ? baseUrl : null;
startupInfo.embedTag = element;
startupInfo.initStartTime = initStartTime;
startupInfo.allowScriptAccess = allowScriptAccess;
startupInfo.pageIndex = 0;
return startupInfo;
}

View File

@ -0,0 +1,56 @@
<!DOCTYPE html>
<!--
Copyright 2015 Mozilla Foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: transparent;
}
iframe {
position:fixed !important;
left:0;top:0;bottom:0;right:0;
overflow: hidden;
line-height: 0;
border: 0px none;
}
body.remoteStopped {
background-color: red;
}
body.remoteDebug {
background-color: green;
}
body.remoteReload {
background-color: yellow;
}
</style>
</head>
<body>
<iframe id="viewer" src="resource://shumway/web/viewer.html" width="100%" height="100%"></iframe>
<script src="chrome://shumway/content/content.js"></script>
</body>
</html>

View File

@ -1,5 +1,5 @@
/*
* Copyright 2014 Mozilla Foundation
* Copyright 2015 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,8 +15,33 @@
*/
Components.utils.import('resource://gre/modules/Services.jsm');
Components.utils.import('resource://gre/modules/Promise.jsm');
Components.utils.import('chrome://shumway/content/ShumwayCom.jsm');
var messageManager, viewerReady;
// Checking if we loading content.js in the OOP/mozbrowser or jsplugins.
// TODO remove mozbrowser logic when we switch to jsplugins only support
if (typeof document === 'undefined') { // mozbrowser OOP frame script
messageManager = this;
viewerReady = Promise.resolve(content);
messageManager.sendAsyncMessage('Shumway:constructed', null);
} else { // jsplugins instance
messageManager = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDocShell)
.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIContentFrameMessageManager);
var viewer = document.getElementById('viewer');
viewerReady = new Promise(function (resolve) {
viewer.addEventListener('load', function () {
messageManager.sendAsyncMessage('Shumway:constructed', null);
resolve(viewer.contentWindow);
});
});
}
var externalInterfaceWrapper = {
callback: function (call) {
if (!shumwayComAdapterHooks.onExternalCallback) {
@ -32,35 +57,37 @@ var shumwayComAdapterHooks = {};
function sendMessage(action, data, sync) {
var detail = {action: action, data: data, sync: sync};
if (!sync) {
sendAsyncMessage('Shumway:message', detail);
messageManager.sendAsyncMessage('Shumway:message', detail);
return;
}
var result = String(sendSyncMessage('Shumway:message', detail));
var result = String(messageManager.sendSyncMessage('Shumway:message', detail));
result = result == 'undefined' ? undefined : JSON.parse(result);
return Components.utils.cloneInto(result, content);
}
function enableDebug() {
sendAsyncMessage('Shumway:enableDebug', null);
messageManager.sendAsyncMessage('Shumway:enableDebug', null);
}
addMessageListener('Shumway:init', function (message) {
messageManager.addMessageListener('Shumway:init', function (message) {
var environment = message.data;
sendAsyncMessage('Shumway:running', {}, {
messageManager.sendAsyncMessage('Shumway:running', {}, {
externalInterface: externalInterfaceWrapper
});
ShumwayCom.createAdapter(content.wrappedJSObject, {
sendMessage: sendMessage,
enableDebug: enableDebug,
getEnvironment: function () { return environment; }
}, shumwayComAdapterHooks);
viewerReady.then(function (viewerWindow) {
ShumwayCom.createAdapter(viewerWindow.wrappedJSObject, {
sendMessage: sendMessage,
enableDebug: enableDebug,
getEnvironment: function () { return environment; }
}, shumwayComAdapterHooks);
content.wrappedJSObject.runViewer();
viewerWindow.wrappedJSObject.runViewer();
});
});
addMessageListener('Shumway:loadFile', function (message) {
messageManager.addMessageListener('Shumway:loadFile', function (message) {
if (!shumwayComAdapterHooks.onLoadFileCallback) {
return;
}

View File

@ -0,0 +1,115 @@
/*
* Copyright 2015 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
Components.utils.import('resource://gre/modules/XPCOMUtils.jsm');
Components.utils.import('resource://gre/modules/Services.jsm');
Components.utils.import('resource://gre/modules/Promise.jsm');
Components.utils.import('chrome://shumway/content/StartupInfo.jsm');
Components.utils.import('chrome://shumway/content/ShumwayCom.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'PrivateBrowsingUtils',
'resource://gre/modules/PrivateBrowsingUtils.jsm');
function log(str) {
var msg = 'plugin.js: ' + str;
Services.console.logStringMessage(msg);
dump(msg + '\n');
}
function runViewer() {
function handlerOOP() {
var frameLoader = pluginElement.frameLoader;
var messageManager = frameLoader.messageManager;
var externalInterface;
messageManager.addMessageListener('Shumway:running', function (message) {
externalInterface = message.objects.externalInterface;
});
messageManager.addMessageListener('Shumway:message', function (message) {
var data = message.data;
var result = shumwayActions.invoke(data.action, data.data);
if (message.sync) {
return result === undefined ? 'undefined' : JSON.stringify(result);
}
});
messageManager.addMessageListener('Shumway:enableDebug', function (message) {
enableDebug();
});
shumwayActions.onExternalCallback = function (call) {
return externalInterface.callback(JSON.stringify(call));
};
shumwayActions.onLoadFileCallback = function (args) {
messageManager.sendAsyncMessage('Shumway:loadFile', args);
};
messageManager.addMessageListener('Shumway:constructed', function (message) {
messageManager.sendAsyncMessage('Shumway:init', getEnvironment());
});
}
function getEnvironment() {
return {
swfUrl: startupInfo.url,
privateBrowsing: startupInfo.privateBrowsing
};
}
function enableDebug() {
DebugUtils.enableDebug(startupInfo.url);
setTimeout(function () {
// TODO fix plugin instance reloading for jsplugins
}, 1000);
}
var startupInfo = getStartupInfo(pluginElement);
if (!startupInfo.url) {
// Special case when movie URL is not specified, e.g. swfobject
// checks only version. No need to instantiate the flash plugin.
if (startupInfo.embedTag) {
setupSimpleExternalInterface(startupInfo.embedTag);
}
return;
}
var document = pluginElement.ownerDocument;
var window = document.defaultView;
var shumwayActions = ShumwayCom.createActions(startupInfo, window, document);
handlerOOP();
// TODO fix remote debugging for jsplugins
}
function setupSimpleExternalInterface(embedTag) {
Components.utils.exportFunction(function (variable) {
switch (variable) {
case '$version':
return 'SHUMWAY 10,0,0';
default:
log('Unsupported GetVariable() call: ' + variable);
return undefined;
}
}, embedTag.wrappedJSObject, {defineAs: 'GetVariable'});
}
runViewer();

View File

@ -1,5 +1,5 @@
/*
* Copyright 2014 Mozilla Foundation
* Copyright 2015 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -22,6 +22,7 @@ const PREF_PREFIX = 'shumway.';
const PREF_IGNORE_CTP = PREF_PREFIX + 'ignoreCTP';
const PREF_WHITELIST = PREF_PREFIX + 'swf.whitelist';
const SWF_CONTENT_TYPE = 'application/x-shockwave-flash';
const PLUGIN_HANLDER_URI = 'chrome://shumway/content/content.html';
let Cc = Components.classes;
let Ci = Components.interfaces;
@ -31,10 +32,7 @@ let Cu = Components.utils;
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
Cu.import('resource://gre/modules/Services.jsm');
Cu.import('resource://shumway/ShumwayStreamConverter.jsm');
let Ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
let registerOverlayPreview = 'registerPlayPreviewMimeType' in Ph;
function getBoolPref(pref, def) {
try {
@ -55,6 +53,7 @@ function getStringPref(pref, def) {
function log(str) {
var msg = 'ShumwayBootstrapUtils.jsm: ' + str;
Services.console.logStringMessage(msg);
dump(msg + '\n');
}
// Register/unregister a constructor as a factory.
@ -78,9 +77,6 @@ Factory.prototype = {
}
};
let converterFactory = new Factory();
let overlayConverterFactory = new Factory();
function allowedPlatformForMedia() {
var oscpu = Cc["@mozilla.org/network/protocol;1?name=http"]
.getService(Ci.nsIHttpProtocolHandler).oscpu;
@ -96,6 +92,7 @@ function allowedPlatformForMedia() {
var ShumwayBootstrapUtils = {
isRegistered: false,
isJSPluginsSupported: false,
register: function () {
if (this.isRegistered) {
@ -105,22 +102,51 @@ var ShumwayBootstrapUtils = {
this.isRegistered = true;
// Register the components.
converterFactory.register(ShumwayStreamConverter);
overlayConverterFactory.register(ShumwayStreamOverlayConverter);
this.isJSPluginsSupported = !!Ph.registerFakePlugin &&
getBoolPref('shumway.jsplugins', false);
if (registerOverlayPreview) {
var ignoreCTP = getBoolPref(PREF_IGNORE_CTP, true);
var whitelist = getStringPref(PREF_WHITELIST);
// Some platforms cannot support video playback, and our whitelist targets
// only video players atm. We need to disable Shumway for those platforms.
if (whitelist && !Services.prefs.prefHasUserValue(PREF_WHITELIST) &&
!allowedPlatformForMedia()) {
log('Default SWF whitelist is used on an unsupported platform -- ' +
'using demo whitelist.');
whitelist = 'http://www.areweflashyet.com/*.swf';
if (this.isJSPluginsSupported) {
let initPluginDict = {
handlerURI: PLUGIN_HANLDER_URI,
mimeEntries: [
{
type: SWF_CONTENT_TYPE,
description: 'Shockwave Flash',
extension: 'swf'
}
],
niceName: 'Shumway plugin',
name: 'Shumway',
supersedeExisting: true, // TODO verify when jsplugins (bug 558184) is implemented
sandboxScript: 'chrome://shumway/content/plugin.js', // TODO verify when jsplugins (bug 558184) is implemented
version: '10.0.0.0'
};
Ph.registerFakePlugin(initPluginDict);
} else {
Cu.import('resource://shumway/ShumwayStreamConverter.jsm');
let converterFactory = new Factory();
converterFactory.register(ShumwayStreamConverter);
this.converterFactory = converterFactory;
let overlayConverterFactory = new Factory();
overlayConverterFactory.register(ShumwayStreamOverlayConverter);
this.overlayConverterFactory = overlayConverterFactory;
let registerOverlayPreview = 'registerPlayPreviewMimeType' in Ph;
if (registerOverlayPreview) {
var ignoreCTP = getBoolPref(PREF_IGNORE_CTP, true);
var whitelist = getStringPref(PREF_WHITELIST);
// Some platforms cannot support video playback, and our whitelist targets
// only video players atm. We need to disable Shumway for those platforms.
if (whitelist && !Services.prefs.prefHasUserValue(PREF_WHITELIST) && !allowedPlatformForMedia()) {
log('Default SWF whitelist is used on an unsupported platform -- ' +
'using demo whitelist.');
whitelist = 'http://www.areweflashyet.com/*.swf';
}
Ph.registerPlayPreviewMimeType(SWF_CONTENT_TYPE, ignoreCTP,
undefined, whitelist);
}
Ph.registerPlayPreviewMimeType(SWF_CONTENT_TYPE, ignoreCTP,
undefined, whitelist);
this.registerOverlayPreview = registerOverlayPreview;
}
},
@ -132,11 +158,17 @@ var ShumwayBootstrapUtils = {
this.isRegistered = false;
// Remove the contract/component.
converterFactory.unregister();
overlayConverterFactory.unregister();
if (this.isJSPluginsSupported) {
Ph.unregisterFakePlugin(PLUGIN_HANLDER_URI);
} else {
this.converterFactory.unregister();
this.converterFactory = null;
this.overlayConverterFactory.unregister();
this.overlayConverterFactory = null;
if (registerOverlayPreview) {
Ph.unregisterPlayPreviewMimeType(SWF_CONTENT_TYPE);
if (this.registerOverlayPreview) {
Ph.unregisterPlayPreviewMimeType(SWF_CONTENT_TYPE);
}
}
}
};

View File

@ -1,5 +1,5 @@
/*
* Copyright 2013 Mozilla Foundation
* Copyright 2015 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -39,6 +39,8 @@ XPCOMUtils.defineLazyModuleGetter(this, 'PrivateBrowsingUtils',
XPCOMUtils.defineLazyModuleGetter(this, 'ShumwayTelemetry',
'resource://shumway/ShumwayTelemetry.jsm');
Components.utils.import('chrome://shumway/content/StartupInfo.jsm');
function getBoolPref(pref, def) {
try {
return Services.prefs.getBoolPref(pref);
@ -60,31 +62,6 @@ function getDOMWindow(aChannel) {
return win;
}
function parseQueryString(qs) {
if (!qs)
return {};
if (qs.charAt(0) == '?')
qs = qs.slice(1);
var values = qs.split('&');
var obj = {};
for (var i = 0; i < values.length; i++) {
var kv = values[i].split('=');
var key = kv[0], value = kv[1];
obj[decodeURIComponent(key)] = decodeURIComponent(value);
}
return obj;
}
function isContentWindowPrivate(win) {
if (!('isContentWindowPrivate' in PrivateBrowsingUtils)) {
return PrivateBrowsingUtils.isWindowPrivate(win);
}
return PrivateBrowsingUtils.isContentWindowPrivate(win);
}
function isShumwayEnabledFor(startupInfo) {
// disabled for PrivateBrowsing windows
if (isContentWindowPrivate(startupInfo.window) &&
@ -224,13 +201,10 @@ ShumwayStreamConverterBase.prototype = {
return requestUrl.spec;
},
getStartupInfo: function(window, urlHint) {
var url = urlHint;
var baseUrl;
var pageUrl;
getStartupInfo: function(window, url) {
var initStartTime = Date.now();
var element = window.frameElement;
var isOverlay = false;
var objectParams = {};
if (element) {
// PlayPreview overlay "belongs" to the embed/object tag and consists of
// DIV and IFRAME. Starting from IFRAME and looking for first object tag.
@ -263,43 +237,14 @@ ShumwayStreamConverterBase.prototype = {
}
if (element) {
// Getting absolute URL from the EMBED tag
url = element.srcURI && element.srcURI.spec;
pageUrl = element.ownerDocument.location.href; // proper page url?
if (tagName == 'EMBED') {
for (var i = 0; i < element.attributes.length; ++i) {
var paramName = element.attributes[i].localName.toLowerCase();
objectParams[paramName] = element.attributes[i].value;
}
} else {
for (var i = 0; i < element.childNodes.length; ++i) {
var paramElement = element.childNodes[i];
if (paramElement.nodeType != 1 ||
paramElement.nodeName != 'PARAM') {
continue;
}
var paramName = paramElement.getAttribute('name').toLowerCase();
objectParams[paramName] = paramElement.getAttribute('value');
}
}
return getStartupInfo(element);
}
baseUrl = pageUrl;
if (objectParams.base) {
try {
// Verifying base URL, passed in object parameters. It shall be okay to
// ignore bad/corrupted base.
var parsedPageUrl = Services.io.newURI(pageUrl);
baseUrl = Services.io.newURI(objectParams.base, null, parsedPageUrl).spec;
} catch (e) { /* it's okay to ignore any exception */ }
}
// Stream converter is used in top level window, just providing basic
// information about SWF.
var objectParams = {};
var movieParams = {};
if (objectParams.flashvars) {
movieParams = parseQueryString(objectParams.flashvars);
}
var queryStringMatch = url && /\?([^#]+)/.exec(url);
if (queryStringMatch) {
var queryStringParams = parseQueryString(queryStringMatch[1]);
@ -310,21 +255,22 @@ ShumwayStreamConverterBase.prototype = {
}
}
var allowScriptAccess = !!url &&
isScriptAllowed(objectParams.allowscriptaccess, url, pageUrl);
// Using the same data structure as we return in StartupInfo.jsm and
// assigning constant values for fields that is not applicable for
// the stream converter when it is used in a top level window.
var startupInfo = {};
startupInfo.window = window;
startupInfo.url = url;
startupInfo.privateBrowsing = isContentWindowPrivate(window);
startupInfo.objectParams = objectParams;
startupInfo.movieParams = movieParams;
startupInfo.baseUrl = baseUrl || url;
startupInfo.isOverlay = isOverlay;
startupInfo.embedTag = element;
startupInfo.isPausedAtStart = /\bpaused=true$/.test(urlHint);
startupInfo.allowScriptAccess = allowScriptAccess;
startupInfo.pageIndex = 0;
startupInfo.baseUrl = url;
startupInfo.isOverlay = false;
startupInfo.refererUrl = null;
startupInfo.embedTag = null;
startupInfo.isPausedAtStart = /\bpaused=true$/.test(url);
startupInfo.initStartTime = initStartTime;
startupInfo.allowScriptAccess = false;
return startupInfo;
},
@ -387,8 +333,7 @@ ShumwayStreamConverterBase.prototype = {
aRequest.cancel(Cr.NS_BINDING_ABORTED);
var domWindow = getDOMWindow(channel);
let startupInfo = converter.getStartupInfo(domWindow,
converter.getUrlHint(originalURI));
let startupInfo = converter.getStartupInfo(domWindow, converter.getUrlHint(originalURI));
listener.onStopRequest(aRequest, context, statusCode);
@ -446,32 +391,6 @@ function setupSimpleExternalInterface(embedTag) {
}, embedTag.wrappedJSObject, {defineAs: 'GetVariable'});
}
function isScriptAllowed(allowScriptAccessParameter, url, pageUrl) {
if (!allowScriptAccessParameter) {
allowScriptAccessParameter = 'sameDomain';
}
var allowScriptAccess = false;
switch (allowScriptAccessParameter.toLowerCase()) { // ignoring case here
case 'always':
allowScriptAccess = true;
break;
case 'never':
allowScriptAccess = false;
break;
default: // 'samedomain'
if (!pageUrl)
break;
try {
// checking if page is in same domain (? same protocol and port)
allowScriptAccess =
Services.io.newURI('/', null, Services.io.newURI(pageUrl, null, null)).spec ==
Services.io.newURI('/', null, Services.io.newURI(url, null, null)).spec;
} catch (ex) {}
break;
}
return allowScriptAccess;
}
// properties required for XPCOM registration:
function copyProperties(obj, template) {
for (var prop in template) {

View File

@ -64,6 +64,10 @@ this.ShumwayTelemetry = {
var histogram = Services.telemetry.getHistogramById("SHUMWAY_FEATURE_USED");
histogram.add(featureType);
},
onLoadResource: function (resultType) {
var histogram = Services.telemetry.getHistogramById("SHUMWAY_LOAD_RESOURCE_RESULT");
histogram.add(resultType);
},
onFallback: function (userAction) {
var histogram = Services.telemetry.getHistogramById("SHUMWAY_FALLBACK");
histogram.add(userAction);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
0.10.346
84cafb5
0.11.422
137ba70

View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<style>
html, body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: transparent;
line-height: 0;
}
#easelContainer {
position:fixed !important;
left:0;top:0;bottom:0;right:0;
overflow: hidden;
line-height: 0;
}
</style>
<script src='resource://shumway/shumway.gfx.js'></script>
</head>
<body contextmenu="shumwayMenu">
<div id="easelContainer"></div>
<menu type="context" id="shumwayMenu">
<menuitem label="Show URL" id="showURLMenu"></menuitem>
<menuitem label="Open in Inspector" id="inspectorMenu"></menuitem>
<menuitem label="Report Problems" id="reportMenu"></menuitem>
<menuitem label="Reload in Adobe Flash Player" id="fallbackMenu" hidden></menuitem>
<menuitem label="Debug this SWF" id="debugMenu"></menuitem>
<menuitem label="About Shumway %version%..." id="aboutMenu"></menuitem>
</menu>
<script src="viewerGfx.js"></script>
</body>
</html>

View File

@ -27,19 +27,16 @@ limitations under the License.
background-color: transparent;
}
body.started {
background-color: transparent;
}
body.started iframe {
body.started #playerIframe {
display: none;
}
#easelContainer {
#gfxIframe {
position:fixed !important;
left:0;top:0;bottom:0;right:0;
left:0; top:0;
width: 100%; height: 100%;
overflow: hidden;
line-height: 0;
border: 0 none;
}
#overlay {
@ -84,7 +81,7 @@ limitations under the License.
background-color: black;
}
#playerWindow {
#playerIframe {
position: absolute;
top: 0;
right: 0;
@ -98,25 +95,16 @@ limitations under the License.
</style>
</head>
<body contextmenu="shumwayMenu">
<iframe id="playerWindow" width="9" height="9" src="" sandbox="allow-scripts"></iframe>
<div id="easelContainer"></div>
<body>
<iframe id="playerIframe" width="9" height="9" src="" sandbox="allow-scripts"></iframe>
<iframe id="gfxIframe" src="" sandbox="allow-scripts"></iframe>
<section>
<div id="overlay">
<a id="fallback" href="#">Shumway <span class="icon">&times;</span></a>
<a id="report" href="#">Report Problems</a>
</div>
<menu type="context" id="shumwayMenu">
<menuitem label="Show URL" id="showURLMenu"></menuitem>
<menuitem label="Open in Inspector" id="inspectorMenu"></menuitem>
<menuitem label="Report Problems" id="reportMenu"></menuitem>
<menuitem label="Reload in Adobe Flash Player" id="fallbackMenu" hidden></menuitem>
<menuitem label="Debug this SWF" id="debugMenu"></menuitem>
<menuitem label="About Shumway %version%..." id="aboutMenu"></menuitem>
</menu>
</section>
<script src='resource://shumway/shumway.gfx.js'></script>
<script src='resource://shumway/web/viewer.js'></script>
</body>
</html>

View File

@ -1,5 +1,5 @@
/*
* Copyright 2013 Mozilla Foundation
* Copyright 2015 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,34 +14,7 @@
* limitations under the License.
*/
function notifyUserInput() {
ShumwayCom.userInput();
}
document.addEventListener('mousedown', notifyUserInput, true);
document.addEventListener('mouseup', notifyUserInput, true);
document.addEventListener('keydown', notifyUserInput, true);
document.addEventListener('keyup', notifyUserInput, true);
function fallback() {
ShumwayCom.fallback();
}
window.print = function(msg) {
console.log(msg);
};
var SHUMWAY_ROOT = "resource://shumway/";
var playerWindow;
var playerWindowLoaded = new Promise(function(resolve) {
var playerWindowIframe = document.getElementById("playerWindow");
playerWindowIframe.addEventListener('load', function () {
playerWindow = playerWindowIframe.contentWindow;
resolve(playerWindowIframe);
});
playerWindowIframe.src = 'resource://shumway/web/viewer.player.html';
});
var movieUrl, movieParams;
function runViewer() {
var flashParams = ShumwayCom.getPluginParams();
@ -53,13 +26,12 @@ function runViewer() {
}
movieParams = flashParams.movieParams;
objectParams = flashParams.objectParams;
var objectParams = flashParams.objectParams;
var baseUrl = flashParams.baseUrl;
var isOverlay = flashParams.isOverlay;
pauseExecution = flashParams.isPausedAtStart;
var isDebuggerEnabled = flashParams.isDebuggerEnabled;
var initStartTime = flashParams.initStartTime;
console.log("url=" + movieUrl + ";params=" + uneval(movieParams));
if (movieParams.fmt_list && movieParams.url_encoded_fmt_stream_map) {
// HACK removing FLVs from the fmt_list
movieParams.fmt_list = movieParams.fmt_list.split(',').filter(function (s) {
@ -68,41 +40,103 @@ function runViewer() {
}).join(',');
}
playerWindowLoaded.then(function (playerWindowIframe) {
ShumwayCom.setupComBridge(playerWindowIframe);
parseSwf(movieUrl, baseUrl, movieParams, objectParams);
var backgroundColor;
if (objectParams) {
var m;
if (objectParams.bgcolor && (m = /#([0-9A-F]{6})/i.exec(objectParams.bgcolor))) {
var hexColor = parseInt(m[1], 16);
backgroundColor = hexColor << 8 | 0xff;
}
if (objectParams.wmode === 'transparent') {
backgroundColor = 0;
}
}
playerReady.then(function () {
var settings = ShumwayCom.getSettings();
var playerSettings = settings.playerSettings;
ShumwayCom.setupPlayerComBridge(document.getElementById('playerIframe'));
parseSwf(movieUrl, baseUrl, movieParams, objectParams, settings, initStartTime, backgroundColor);
if (isOverlay) {
if (isDebuggerEnabled) {
document.getElementById('overlay').className = 'enabled';
var fallbackDiv = document.getElementById('fallback');
fallbackDiv.addEventListener('click', function (e) {
fallback();
e.preventDefault();
});
var reportDiv = document.getElementById('report');
reportDiv.addEventListener('click', function (e) {
reportIssue();
e.preventDefault();
});
}
}
ShumwayCom.setupGfxComBridge(document.getElementById('gfxIframe'));
gfxWindow.postMessage({
type: 'prepareUI',
params: {
isOverlay: isOverlay,
isDebuggerEnabled: isDebuggerEnabled,
isHudOn: playerSettings.hud,
backgroundColor: backgroundColor
}
}, '*')
});
}
if (isOverlay) {
document.getElementById('overlay').className = 'enabled';
var fallbackDiv = document.getElementById('fallback');
fallbackDiv.addEventListener('click', function(e) {
fallback();
e.preventDefault();
});
var reportDiv = document.getElementById('report');
reportDiv.addEventListener('click', function(e) {
reportIssue();
e.preventDefault();
});
var fallbackMenu = document.getElementById('fallbackMenu');
fallbackMenu.removeAttribute('hidden');
fallbackMenu.addEventListener('click', fallback);
window.addEventListener("message", function handlerMessage(e) {
var args = e.data;
if (typeof args !== 'object' || args === null) {
return;
}
document.getElementById('showURLMenu').addEventListener('click', showURL);
document.getElementById('inspectorMenu').addEventListener('click', showInInspector);
document.getElementById('reportMenu').addEventListener('click', reportIssue);
document.getElementById('aboutMenu').addEventListener('click', showAbout);
var version = Shumway.version || '';
document.getElementById('aboutMenu').label =
document.getElementById('aboutMenu').label.replace('%version%', version);
if (isDebuggerEnabled) {
document.getElementById('debugMenu').addEventListener('click', enableDebug);
} else {
document.getElementById('debugMenu').remove();
if (gfxWindow && e.source === gfxWindow) {
switch (args.callback) {
case 'displayParameters':
// The display parameters data will be send to the player window.
// TODO do we need sanitize it?
displayParametersResolved(args.params);
break;
case 'showURL':
showURL();
break;
case 'showInInspector':
showInInspector();
break;
case 'reportIssue':
reportIssue();
break;
case 'showAbout':
showAbout();
break;
case 'enableDebug':
enableDebug();
break;
case 'fallback':
fallback();
break;
default:
console.error('Unexpected message from gfx frame: ' + args.callback);
break;
}
}
if (playerWindow && e.source === playerWindow) {
switch (args.callback) {
case 'started':
document.body.classList.add('started');
break;
default:
console.error('Unexpected message from player frame: ' + args.callback);
break;
}
}
}, true);
function fallback() {
ShumwayCom.fallback();
}
function showURL() {
@ -147,72 +181,54 @@ function enableDebug() {
ShumwayCom.enableDebug();
}
var movieUrl, movieParams, objectParams;
var playerWindow, gfxWindow;
window.addEventListener("message", function handlerMessage(e) {
var args = e.data;
switch (args.callback) {
case 'started':
document.body.classList.add('started');
break;
}
}, true);
var easelHost;
function parseSwf(url, baseUrl, movieParams, objectParams) {
var settings = ShumwayCom.getSettings();
function parseSwf(url, baseUrl, movieParams, objectParams, settings,
initStartTime, backgroundColor) {
var compilerSettings = settings.compilerSettings;
var playerSettings = settings.playerSettings;
// init misc preferences
Shumway.GFX.hud.value = playerSettings.hud;
//forceHidpi.value = settings.playerSettings.forceHidpi;
displayParametersReady.then(function (displayParameters) {
var data = {
type: 'runSwf',
flashParams: {
compilerSettings: compilerSettings,
movieParams: movieParams,
objectParams: objectParams,
displayParameters: displayParameters,
turboMode: playerSettings.turboMode,
env: playerSettings.env,
bgcolor: backgroundColor,
url: url,
baseUrl: baseUrl || url,
initStartTime: initStartTime
}
};
playerWindow.postMessage(data, '*');
});
}
console.info("Compiler settings: " + JSON.stringify(compilerSettings));
console.info("Parsing " + url + "...");
// We need to wait for gfx window to report display parameters before we
// start SWF playback in the player window.
var displayParametersResolved;
var displayParametersReady = new Promise(function (resolve) {
displayParametersResolved = resolve;
});
var backgroundColor;
if (objectParams) {
var m;
if (objectParams.bgcolor && (m = /#([0-9A-F]{6})/i.exec(objectParams.bgcolor))) {
var hexColor = parseInt(m[1], 16);
backgroundColor = hexColor << 8 | 0xff;
}
if (objectParams.wmode === 'transparent') {
backgroundColor = 0;
var playerReady = new Promise(function (resolve) {
function iframeLoaded() {
if (--iframesToLoad > 0) {
return;
}
gfxWindow = document.getElementById('gfxIframe').contentWindow;
playerWindow = document.getElementById('playerIframe').contentWindow;
resolve();
}
var easel = createEasel(backgroundColor);
easelHost = new Shumway.GFX.Window.WindowEaselHost(easel, playerWindow, window);
var displayParameters = easel.getDisplayParameters();
var data = {
type: 'runSwf',
settings: Shumway.Settings.getSettings(),
flashParams: {
compilerSettings: compilerSettings,
movieParams: movieParams,
objectParams: objectParams,
displayParameters: displayParameters,
turboMode: playerSettings.turboMode,
env: playerSettings.env,
bgcolor: backgroundColor,
url: url,
baseUrl: baseUrl || url
}
};
playerWindow.postMessage(data, '*');
}
function createEasel(backgroundColor) {
var Stage = Shumway.GFX.Stage;
var Easel = Shumway.GFX.Easel;
var Canvas2DRenderer = Shumway.GFX.Canvas2DRenderer;
Shumway.GFX.WebGL.SHADER_ROOT = SHUMWAY_ROOT + "gfx/gl/shaders/";
var easel = new Easel(document.getElementById("easelContainer"), false, backgroundColor);
easel.startRendering();
return easel;
}
var iframesToLoad = 2;
document.getElementById('gfxIframe').addEventListener('load', iframeLoaded);
document.getElementById('gfxIframe').src = 'resource://shumway/web/viewer.gfx.html';
document.getElementById('playerIframe').addEventListener('load', iframeLoaded);
document.getElementById('playerIframe').src = 'resource://shumway/web/viewer.player.html';
});

View File

@ -0,0 +1,122 @@
/*
* Copyright 2013 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var SHUMWAY_ROOT = "resource://shumway/";
var easel;
function createEasel(backgroundColor) {
var Stage = Shumway.GFX.Stage;
var Easel = Shumway.GFX.Easel;
var Canvas2DRenderer = Shumway.GFX.Canvas2DRenderer;
Shumway.GFX.WebGL.SHADER_ROOT = SHUMWAY_ROOT + "gfx/gl/shaders/";
easel = new Easel(document.getElementById("easelContainer"), false, backgroundColor);
if (ShumwayCom.environment === 'test') {
ShumwayCom.setScreenShotCallback(function () {
// flush rendering buffers
easel.render();
return easel.screenShot(null, true, false).dataURL;
});
}
easel.startRendering();
return easel;
}
var easelHost;
function createEaselHost() {
var peer = new Shumway.Remoting.ShumwayComTransportPeer();
easelHost = new Shumway.GFX.Window.WindowEaselHost(easel, peer);
return easelHost;
}
function setHudVisible(visible) {
Shumway.GFX.hud.value = !!visible;
}
function fallback() {
parent.postMessage({callback: 'fallback'}, '*');
}
function showURL() {
parent.postMessage({callback: 'showURL'}, '*' );
}
function showInInspector() {
parent.postMessage({callback: 'showInInspector'}, '*');
}
function reportIssue() {
parent.postMessage({callback: 'reportIssue'}, '*');
}
function showAbout() {
parent.postMessage({callback: 'showAbout'}, '*');
}
function enableDebug() {
parent.postMessage({callback: 'enableDebug'}, '*');
}
function prepareUI(params) {
if (params.isOverlay) {
var fallbackMenu = document.getElementById('fallbackMenu');
fallbackMenu.removeAttribute('hidden');
fallbackMenu.addEventListener('click', fallback);
}
document.getElementById('showURLMenu').addEventListener('click', showURL);
document.getElementById('inspectorMenu').addEventListener('click', showInInspector);
document.getElementById('reportMenu').addEventListener('click', reportIssue);
document.getElementById('aboutMenu').addEventListener('click', showAbout);
var version = Shumway.version || '';
document.getElementById('aboutMenu').label =
document.getElementById('aboutMenu').label.replace('%version%', version);
if (params.isDebuggerEnabled) {
document.getElementById('debugMenu').addEventListener('click', enableDebug);
} else {
document.getElementById('debugMenu').remove();
}
setHudVisible(params.isHudOn);
createEasel(params.backgroundColor);
createEaselHost();
var displayParameters = easel.getDisplayParameters();
window.parent.postMessage({
callback: 'displayParameters',
params: displayParameters
}, '*');
}
window.addEventListener('message', function onWindowMessage(e) {
var data = e.data;
if (typeof data !== 'object' || data === null) {
console.error('Unexpected message for gfx frame.');
return;
}
switch (data.type) {
case "prepareUI":
prepareUI(data.params);
break;
default:
console.error('Unexpected message for gfx frame: ' + args.callback);
break;
}
}, true);

View File

@ -14,38 +14,52 @@
* limitations under the License.
*/
var release = true;
window.print = function(msg) {
console.log(msg);
};
function runSwfPlayer(flashParams) {
var EXECUTION_MODE = Shumway.AVM2.Runtime.ExecutionMode;
function runSwfPlayer(flashParams, settings) {
console.info('Time from init start to SWF player start: ' + (Date.now() - flashParams.initStartTime));
if (settings) {
Shumway.Settings.setSettings(settings);
}
setupServices();
var compilerSettings = flashParams.compilerSettings;
var sysMode = compilerSettings.sysCompiler ? EXECUTION_MODE.COMPILE : EXECUTION_MODE.INTERPRET;
var appMode = compilerSettings.appCompiler ? EXECUTION_MODE.COMPILE : EXECUTION_MODE.INTERPRET;
var asyncLoading = true;
var baseUrl = flashParams.baseUrl;
var objectParams = flashParams.objectParams;
var movieUrl = flashParams.url;
Shumway.frameRateOption.value = flashParams.turboMode ? 60 : -1;
Shumway.AVM2.Verifier.enabled.value = compilerSettings.verifier;
if (ShumwayCom.environment === 'test') {
Shumway.frameRateOption.value = 60;
Shumway.dontSkipFramesOption.value = true;
window.print = function(msg) {
ShumwayCom.print(msg.toString());
};
Shumway.Random.reset();
Shumway.installTimeWarper();
} else {
Shumway.frameRateOption.value = flashParams.turboMode ? 60 : -1;
}
Shumway.createAVM2(Shumway.AVM2LoadLibrariesFlags.Builtin | Shumway.AVM2LoadLibrariesFlags.Playerglobal, sysMode, appMode).then(function (avm2) {
Shumway.createSecurityDomain(Shumway.AVM2LoadLibrariesFlags.Builtin | Shumway.AVM2LoadLibrariesFlags.Playerglobal).then(function (securityDomain) {
function runSWF(file, buffer, baseUrl) {
var gfxService = new Shumway.Player.Window.WindowGFXService(window, window.parent);
var player = new Shumway.Player.Player(gfxService, flashParams.env);
var peer = new Shumway.Remoting.ShumwayComTransportPeer();
var gfxService = new Shumway.Player.Window.WindowGFXService(securityDomain, peer);
var player = new Shumway.Player.Player(securityDomain, gfxService, flashParams.env);
player.defaultStageColor = flashParams.bgcolor;
player.movieParams = flashParams.movieParams;
player.stageAlign = (objectParams && (objectParams.salign || objectParams.align)) || '';
player.stageScale = (objectParams && objectParams.scale) || 'showall';
player.displayParameters = flashParams.displayParameters;
player.initStartTime = flashParams.initStartTime;
player.pageUrl = baseUrl;
console.info('Time from init start to SWF loading start: ' + (Date.now() - flashParams.initStartTime));
player.load(file, buffer);
playerStarted();
}
Shumway.FileLoadingService.instance.init(baseUrl);
@ -68,11 +82,20 @@ function setupServices() {
Shumway.ClipboardService.instance = new Shumway.Player.ShumwayComClipboardService();
Shumway.FileLoadingService.instance = new Shumway.Player.ShumwayComFileLoadingService();
Shumway.SystemResourcesLoadingService.instance = new Shumway.Player.ShumwayComResourcesLoadingService(true);
Shumway.LocalConnectionService.instance = new Shumway.Player.ShumwayComLocalConnectionService();
}
function playerStarted() {
document.body.style.backgroundColor = 'green';
window.parent.postMessage({
callback: 'started'
}, '*');
}
window.addEventListener('message', function onWindowMessage(e) {
var data = e.data;
if (typeof data !== 'object' || data === null) {
console.error('Unexpected message for player frame.');
return;
}
switch (data.type) {
@ -81,12 +104,10 @@ window.addEventListener('message', function onWindowMessage(e) {
Shumway.Settings.setSettings(data.settings);
}
setupServices();
runSwfPlayer(data.flashParams);
document.body.style.backgroundColor = 'green';
window.parent.postMessage({
callback: 'started'
}, '*');
runSwfPlayer(data.flashParams, data.settings);
break;
default:
console.error('Unexpected message for player frame: ' + args.callback);
break;
}
}, true);

View File

@ -41,6 +41,7 @@ const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
const PREF_SESSIONS_BRANCH = "datareporting.sessions.";
const PREF_UNIFIED = PREF_BRANCH + "unified";
const PREF_UNIFIED_OPTIN = PREF_BRANCH + "unifiedIsOptIn";
const PREF_OPTOUT_SAMPLE = PREF_BRANCH + "optoutSample";
// Whether the FHR/Telemetry unification features are enabled.
// Changing this pref requires a restart.
@ -90,6 +91,29 @@ XPCOMUtils.defineLazyModuleGetter(this, "TelemetrySend",
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryReportingPolicy",
"resource://gre/modules/TelemetryReportingPolicy.jsm");
XPCOMUtils.defineLazyGetter(this, "gCrcTable", function() {
let c;
let table = [];
for (let n = 0; n < 256; n++) {
c = n;
for (let k =0; k < 8; k++) {
c = ((c&1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
}
table[n] = c;
}
return table;
});
function crc32(str) {
let crc = 0 ^ (-1);
for (let i = 0; i < str.length; i++ ) {
crc = (crc >>> 8) ^ gCrcTable[(crc ^ str.charCodeAt(i)) & 0xFF];
}
return (crc ^ (-1)) >>> 0;
}
/**
* Setup Telemetry logging. This function also gets called when loggin related
* preferences change.
@ -129,6 +153,8 @@ function configureLogging() {
let Policy = {
now: () => new Date(),
generatePingId: () => Utils.generateUUID(),
getCachedClientID: () => ClientID.getCachedClientID(),
isUnifiedOptin: () => IS_UNIFIED_OPTIN,
}
this.EXPORTED_SYMBOLS = ["TelemetryController"];
@ -304,6 +330,15 @@ this.TelemetryController = Object.freeze({
return Impl.clientID;
},
/**
* Whether this client is part of a sample that gets opt-out Telemetry.
*
* @return {Boolean} Whether the client is part of the opt-out sample.
*/
get isInOptoutSample() {
return Impl.isInOptoutSample;
},
/**
* The AsyncShutdown.Barrier to synchronize with TelemetryController shutdown.
*/
@ -587,6 +622,34 @@ let Impl = {
return TelemetryStorage.removeAbortedSessionPing();
},
/**
*
*/
_isInOptoutSample: function() {
if (!Preferences.get(PREF_OPTOUT_SAMPLE, false)) {
this._log.config("_sampleForOptoutTelemetry - optout sampling is disabled");
return false;
}
const clientId = Policy.getCachedClientID();
if (!clientId) {
this._log.config("_sampleForOptoutTelemetry - no cached client id available")
return false;
}
// This mimics the server-side 1% sampling, so that we can get matching populations.
// The server samples on ((crc32(clientId) % 100) == 42), we match 42+X here to get
// a bigger sample.
const sample = crc32(clientId) % 100;
const offset = 42;
const range = 5; // sampling from 5%
const optout = (sample >= offset && sample < (offset + range));
this._log.config("_sampleForOptoutTelemetry - sampling for optout Telemetry - " +
"offset: " + offset + ", range: " + range + ", sample: " + sample);
return optout;
},
/**
* Perform telemetry initialization for either chrome or content process.
* @return {Boolean} True if Telemetry is allowed to record at least base (FHR) data,
@ -605,9 +668,11 @@ let Impl = {
// Configure base Telemetry recording.
// Unified Telemetry makes it opt-out unless the unifedOptin pref is set.
// Additionally, we make Telemetry opt-out for a 5% sample.
// If extended Telemetry is enabled, base recording is always on as well.
const enabled = Preferences.get(PREF_ENABLED, false);
Telemetry.canRecordBase = enabled || (IS_UNIFIED_TELEMETRY && !IS_UNIFIED_OPTIN);
const isOptout = IS_UNIFIED_TELEMETRY && (!Policy.isUnifiedOptin() || this._isInOptoutSample());
Telemetry.canRecordBase = enabled || isOptout;
#ifdef MOZILLA_OFFICIAL
// Enable extended telemetry if:
@ -678,7 +743,7 @@ let Impl = {
// id from disk.
// We try to cache it in prefs to avoid this, even though this may
// lead to some stale client ids.
this._clientID = Preferences.get(PREF_CACHED_CLIENTID, null);
this._clientID = ClientID.getCachedClientID();
// Delay full telemetry initialization to give the browser time to
// run various late initializers. Otherwise our gathered memory
@ -691,9 +756,8 @@ let Impl = {
yield TelemetrySend.setup(this._testMode);
// Load the ClientID and update the cache.
// Load the ClientID.
this._clientID = yield ClientID.getClientID();
Preferences.set(PREF_CACHED_CLIENTID, this._clientID);
// Purge the pings archive by removing outdated pings. We don't wait for this
// task to complete, but TelemetryStorage blocks on it during shutdown.
@ -816,6 +880,10 @@ let Impl = {
return this._clientID;
},
get isInOptoutSample() {
return this._isInOptoutSample();
},
/**
* Get an object describing the current state of this module for AsyncShutdown diagnostics.
*/

View File

@ -19,6 +19,7 @@ Cu.import("resource://gre/modules/PromiseUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/TelemetryUtils.jsm", this);
Cu.import("resource://gre/modules/ObjectUtils.jsm");
Cu.import("resource://gre/modules/TelemetryController.jsm", this);
const Utils = TelemetryUtils;
@ -1002,6 +1003,7 @@ EnvironmentCache.prototype = {
#endif
e10sEnabled: Services.appinfo.browserTabsRemoteAutostart,
telemetryEnabled: Preferences.get(PREF_TELEMETRY_ENABLED, false),
isInOptoutSample: TelemetryController.isInOptoutSample,
locale: getBrowserLocale(),
update: {
channel: updateChannel,

View File

@ -724,14 +724,6 @@ this.TelemetrySession = Object.freeze({
observe: function (aSubject, aTopic, aData) {
return Impl.observe(aSubject, aTopic, aData);
},
/**
* The client id send with the telemetry ping.
*
* @return The client id as string, or null.
*/
get clientID() {
return Impl.clientID;
},
});
let Impl = {

View File

@ -42,6 +42,7 @@ Structure::
},
e10sEnabled: <bool>, // whether e10s is on, i.e. browser tabs open by default in a different process
telemetryEnabled: <bool>, // false on failure
isInOptoutSample: <bool>, // whether this client is part of the opt-out sample
locale: <string>, // e.g. "it", null on failure
update: {
channel: <string>, // e.g. "release", null on failure

View File

@ -274,6 +274,16 @@ function fakeGeneratePingId(func) {
module.Policy.generatePingId = func;
}
function fakeCachedClientId(uuid) {
let module = Cu.import("resource://gre/modules/TelemetryController.jsm");
module.Policy.getCachedClientID = () => uuid;
}
function fakeIsUnifiedOptin(isOptin) {
let module = Cu.import("resource://gre/modules/TelemetryController.jsm");
module.Policy.isUnifiedOptin = () => isOptin;
}
// Return a date that is |offset| ms in the future from |date|.
function futureDate(date, offset) {
return new Date(date.getTime() + offset);

View File

@ -33,6 +33,7 @@ const PREF_ARCHIVE_ENABLED = PREF_BRANCH + "archive.enabled";
const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
const PREF_FHR_SERVICE_ENABLED = "datareporting.healthreport.service.enabled";
const PREF_UNIFIED = PREF_BRANCH + "unified";
const PREF_OPTOUT_SAMPLE = PREF_BRANCH + "optoutSample";
let gClientID = null;
@ -368,6 +369,64 @@ add_task(function* test_changePingAfterSubmission() {
"The payload must not be changed after being submitted.");
});
add_task(function* test_optoutSampling() {
if (!Preferences.get(PREF_UNIFIED, false)) {
dump("Unified Telemetry is disabled, skipping.\n");
return;
}
const DATA = [
{uuid: null, sampled: false}, // not to be sampled
{uuid: "3d38d821-14a4-3d45-ab0b-02a9fb5a7505", sampled: false}, // samples to 0
{uuid: "1331255e-7eb5-aa4f-b04e-494a0c6da282", sampled: false}, // samples to 41
{uuid: "35393e78-a363-ea4e-9fc9-9f9abbee2077", sampled: true }, // samples to 42
{uuid: "4dc81df6-db03-a34e-ba79-3e877afd22c4", sampled: true }, // samples to 43
{uuid: "79e15be6-4884-8d4f-98e5-f94790251e5f", sampled: true }, // samples to 44
{uuid: "c3841566-e39e-384d-826f-508ab6387b21", sampled: true }, // samples to 45
{uuid: "cc7498a4-2cde-da47-89b3-f3ce5dd7c6fc", sampled: true }, // samples to 46
{uuid: "0750d8ed-5969-3a4f-90ba-2e85f9074309", sampled: false}, // samples to 47
{uuid: "0dfcbce7-d82b-b144-8d77-eb15935c9a8e", sampled: false}, // samples to 99
];
// Test that the opt-out pref enables us sampling on 5% of release.
Preferences.set(PREF_ENABLED, false);
Preferences.set(PREF_OPTOUT_SAMPLE, true);
fakeIsUnifiedOptin(true);
for (let d of DATA) {
dump("Testing sampling for uuid: " + d.uuid + "\n");
fakeCachedClientId(d.uuid);
yield TelemetryController.reset();
Assert.equal(TelemetryController.isInOptoutSample, d.sampled,
"Opt-out sampling should behave as expected");
Assert.equal(Telemetry.canRecordBase, d.sampled,
"Base recording setting should be correct");
}
// If we disable opt-out sampling Telemetry, have the opt-in setting on and extended Telemetry off,
// we should not enable anything.
Preferences.set(PREF_OPTOUT_SAMPLE, false);
fakeIsUnifiedOptin(true);
for (let d of DATA) {
dump("Testing sampling for uuid: " + d.uuid + "\n");
fakeCachedClientId(d.uuid);
yield TelemetryController.reset();
Assert.equal(Telemetry.canRecordBase, false,
"Sampling should not override the default opt-out behavior");
}
// If we fully enable opt-out Telemetry on release, the sampling should not override that.
Preferences.set(PREF_OPTOUT_SAMPLE, true);
fakeIsUnifiedOptin(false);
for (let d of DATA) {
dump("Testing sampling for uuid: " + d.uuid + "\n");
fakeCachedClientId(d.uuid);
yield TelemetryController.reset();
Assert.equal(Telemetry.canRecordBase, true,
"Sampling should not override the default opt-out behavior");
}
});
add_task(function* stopServer(){
yield PingServer.stop();
do_test_finished();

View File

@ -255,6 +255,7 @@ function checkSettingsSection(data) {
blocklistEnabled: "boolean",
e10sEnabled: "boolean",
telemetryEnabled: "boolean",
isInOptoutSample: "boolean",
locale: "string",
update: "object",
userPrefs: "object",

View File

@ -11,6 +11,7 @@ const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
"resource://services-common/utils.js");
@ -23,6 +24,8 @@ XPCOMUtils.defineLazyGetter(this, "gStateFilePath", () => {
return OS.Path.join(gDatareportingPath, "state.json");
});
const PREF_CACHED_CLIENTID = "toolkit.telemetry.cachedClientID";
this.ClientID = Object.freeze({
/**
* This returns a promise resolving to the the stable client ID we use for
@ -35,6 +38,17 @@ this.ClientID = Object.freeze({
return ClientIDImpl.getClientID();
},
/**
* Get the client id synchronously without hitting the disk.
* This returns:
* - the current on-disk client id if it was already loaded
* - the client id that we cached into preferences (if any)
* - null otherwise
*/
getCachedClientID: function() {
return ClientIDImpl.getCachedClientID();
},
/**
* Only used for testing. Invalidates the client ID so that it gets read
* again from file.
@ -71,6 +85,7 @@ let ClientIDImpl = {
let state = yield CommonUtils.readJSON(gStateFilePath);
if (state && 'clientID' in state && typeof(state.clientID) == 'string') {
this._clientID = state.clientID;
Preferences.set(PREF_CACHED_CLIENTID, this._clientID);
return this._clientID;
}
} catch (e) {
@ -83,6 +98,7 @@ let ClientIDImpl = {
let state = yield CommonUtils.readJSON(fhrStatePath);
if (state && 'clientID' in state && typeof(state.clientID) == 'string') {
this._clientID = state.clientID;
Preferences.set(PREF_CACHED_CLIENTID, this._clientID);
this._saveClientID();
return this._clientID;
}
@ -92,6 +108,7 @@ let ClientIDImpl = {
// We dont have an id from FHR yet, generate a new ID.
this._clientID = CommonUtils.generateUUID();
Preferences.set(PREF_CACHED_CLIENTID, this._clientID);
this._saveClientIdTask = this._saveClientID();
// Wait on persisting the id. Otherwise failure to save the ID would result in
@ -130,6 +147,23 @@ let ClientIDImpl = {
return Promise.resolve(this._clientID);
},
/**
* Get the client id synchronously without hitting the disk.
* This returns:
* - the current on-disk client id if it was already loaded
* - the client id that we cached into preferences (if any)
* - null otherwise
*/
getCachedClientID: function() {
if (this._clientID) {
// Already loaded the client id from disk.
return this._clientID;
}
// Not yet loaded, return the cached client id if we have one.
return Preferences.get(PREF_CACHED_CLIENTID, null);
},
/*
* Resets the provider. This is for testing only.
*/