зеркало из https://github.com/mozilla/gecko-dev.git
1084 строки
35 KiB
JavaScript
1084 строки
35 KiB
JavaScript
/*
|
|
* 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.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
var EXPORTED_SYMBOLS = ['ShumwayStreamConverter', 'ShumwayStreamOverlayConverter'];
|
|
|
|
const Cc = Components.classes;
|
|
const Ci = Components.interfaces;
|
|
const Cr = Components.results;
|
|
const Cu = Components.utils;
|
|
|
|
const SHUMWAY_CONTENT_TYPE = 'application/x-shockwave-flash';
|
|
const EXPECTED_PLAYPREVIEW_URI_PREFIX = 'data:application/x-moz-playpreview;,' +
|
|
SHUMWAY_CONTENT_TYPE;
|
|
|
|
const FIREFOX_ID = '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}';
|
|
const SEAMONKEY_ID = '{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}';
|
|
|
|
const MAX_CLIPBOARD_DATA_SIZE = 8000;
|
|
const MAX_USER_INPUT_TIMEOUT = 250; // ms
|
|
|
|
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
|
|
Cu.import('resource://gre/modules/Services.jsm');
|
|
Cu.import('resource://gre/modules/NetUtil.jsm');
|
|
Cu.import('resource://gre/modules/Promise.jsm');
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, 'PrivateBrowsingUtils',
|
|
'resource://gre/modules/PrivateBrowsingUtils.jsm');
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, 'AddonManager',
|
|
'resource://gre/modules/AddonManager.jsm');
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, 'ShumwayTelemetry',
|
|
'resource://shumway/ShumwayTelemetry.jsm');
|
|
|
|
let Svc = {};
|
|
XPCOMUtils.defineLazyServiceGetter(Svc, 'mime',
|
|
'@mozilla.org/mime;1', 'nsIMIMEService');
|
|
|
|
let StringInputStream = Cc["@mozilla.org/io/string-input-stream;1"];
|
|
let MimeInputStream = Cc["@mozilla.org/network/mime-input-stream;1"];
|
|
|
|
function getBoolPref(pref, def) {
|
|
try {
|
|
return Services.prefs.getBoolPref(pref);
|
|
} catch (ex) {
|
|
return def;
|
|
}
|
|
}
|
|
|
|
function getStringPref(pref, def) {
|
|
try {
|
|
return Services.prefs.getComplexValue(pref, Ci.nsISupportsString).data;
|
|
} catch (ex) {
|
|
return def;
|
|
}
|
|
}
|
|
|
|
function log(aMsg) {
|
|
let msg = 'ShumwayStreamConverter.js: ' + (aMsg.join ? aMsg.join('') : aMsg);
|
|
Services.console.logStringMessage(msg);
|
|
dump(msg + '\n');
|
|
}
|
|
|
|
function getDOMWindow(aChannel) {
|
|
var requestor = aChannel.notificationCallbacks ||
|
|
aChannel.loadGroup.notificationCallbacks;
|
|
var win = requestor.getInterface(Components.interfaces.nsIDOMWindow);
|
|
return win;
|
|
}
|
|
|
|
function makeContentReadable(obj, window) {
|
|
if (Cu.cloneInto) {
|
|
return Cu.cloneInto(obj, window);
|
|
}
|
|
// TODO remove for Firefox 32+
|
|
if (typeof obj !== 'object' || obj === null) {
|
|
return obj;
|
|
}
|
|
var expose = {};
|
|
for (let k in obj) {
|
|
expose[k] = "rw";
|
|
}
|
|
obj.__exposedProps__ = expose;
|
|
return obj;
|
|
}
|
|
|
|
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 domainMatches(host, pattern) {
|
|
if (!pattern) return false;
|
|
if (pattern === '*') return true;
|
|
host = host.toLowerCase();
|
|
var parts = pattern.toLowerCase().split('*');
|
|
if (host.indexOf(parts[0]) !== 0) return false;
|
|
var p = parts[0].length;
|
|
for (var i = 1; i < parts.length; i++) {
|
|
var j = host.indexOf(parts[i], p);
|
|
if (j === -1) return false;
|
|
p = j + parts[i].length;
|
|
}
|
|
return parts[parts.length - 1] === '' || p === host.length;
|
|
}
|
|
|
|
function fetchPolicyFile(url, cache, callback) {
|
|
if (url in cache) {
|
|
return callback(cache[url]);
|
|
}
|
|
|
|
log('Fetching policy file at ' + url);
|
|
var MAX_POLICY_SIZE = 8192;
|
|
var xhr = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
|
.createInstance(Ci.nsIXMLHttpRequest);
|
|
xhr.open('GET', url, true);
|
|
xhr.overrideMimeType('text/xml');
|
|
xhr.onprogress = function (e) {
|
|
if (e.loaded >= MAX_POLICY_SIZE) {
|
|
xhr.abort();
|
|
cache[url] = false;
|
|
callback(null, 'Max policy size');
|
|
}
|
|
};
|
|
xhr.onreadystatechange = function(event) {
|
|
if (xhr.readyState === 4) {
|
|
// TODO disable redirects
|
|
var doc = xhr.responseXML;
|
|
if (xhr.status !== 200 || !doc) {
|
|
cache[url] = false;
|
|
return callback(null, 'Invalid HTTP status: ' + xhr.statusText);
|
|
}
|
|
// parsing params
|
|
var params = doc.documentElement.childNodes;
|
|
var policy = { siteControl: null, allowAccessFrom: []};
|
|
for (var i = 0; i < params.length; i++) {
|
|
switch (params[i].localName) {
|
|
case 'site-control':
|
|
policy.siteControl = params[i].getAttribute('permitted-cross-domain-policies');
|
|
break;
|
|
case 'allow-access-from':
|
|
var access = {
|
|
domain: params[i].getAttribute('domain'),
|
|
security: params[i].getAttribute('security') === 'true'
|
|
};
|
|
policy.allowAccessFrom.push(access);
|
|
break;
|
|
default:
|
|
// TODO allow-http-request-headers-from and other
|
|
break;
|
|
}
|
|
}
|
|
callback(cache[url] = policy);
|
|
}
|
|
};
|
|
xhr.send(null);
|
|
}
|
|
|
|
function isContentWindowPrivate(win) {
|
|
if (!('isContentWindowPrivate' in PrivateBrowsingUtils)) {
|
|
return PrivateBrowsingUtils.isWindowPrivate(win);
|
|
}
|
|
return PrivateBrowsingUtils.isContentWindowPrivate(win);
|
|
}
|
|
|
|
function isShumwayEnabledFor(actions) {
|
|
// disabled for PrivateBrowsing windows
|
|
if (isContentWindowPrivate(actions.window) &&
|
|
!getBoolPref('shumway.enableForPrivate', false)) {
|
|
return false;
|
|
}
|
|
// disabled if embed tag specifies shumwaymode (for testing purpose)
|
|
if (actions.objectParams['shumwaymode'] === 'off') {
|
|
return false;
|
|
}
|
|
|
|
var url = actions.url;
|
|
var baseUrl = actions.baseUrl;
|
|
|
|
// blacklisting well known sites with issues
|
|
if (/\.ytimg\.com\//i.test(url) /* youtube movies */ ||
|
|
/\/vui.swf\b/i.test(url) /* vidyo manager */ ||
|
|
/soundcloud\.com\/player\/assets\/swf/i.test(url) /* soundcloud */ ||
|
|
/sndcdn\.com\/assets\/swf/.test(url) /* soundcloud */ ||
|
|
/vimeocdn\.com/.test(url) /* vimeo */) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function getVersionInfo() {
|
|
var deferred = Promise.defer();
|
|
var versionInfo = {
|
|
version: 'unknown',
|
|
geckoBuildID: 'unknown',
|
|
shumwayVersion: 'unknown'
|
|
};
|
|
try {
|
|
var appInfo = Components.classes["@mozilla.org/xre/app-info;1"]
|
|
.getService(Components.interfaces.nsIXULAppInfo);
|
|
versionInfo.geckoVersion = appInfo.version;
|
|
versionInfo.geckoBuildID = appInfo.appBuildID;
|
|
} catch (e) {
|
|
log('Error encountered while getting platform version info:', e);
|
|
}
|
|
try {
|
|
var addonId = "shumway@research.mozilla.org";
|
|
AddonManager.getAddonByID(addonId, function(addon) {
|
|
versionInfo.shumwayVersion = addon ? addon.version : 'n/a';
|
|
deferred.resolve(versionInfo);
|
|
});
|
|
} catch (e) {
|
|
log('Error encountered while getting Shumway version info:', e);
|
|
deferred.resolve(versionInfo);
|
|
}
|
|
return deferred.promise;
|
|
}
|
|
|
|
function fallbackToNativePlugin(window, userAction, activateCTP) {
|
|
var obj = window.frameElement;
|
|
var doc = obj.ownerDocument;
|
|
var e = doc.createEvent("CustomEvent");
|
|
e.initCustomEvent("MozPlayPlugin", true, true, activateCTP);
|
|
obj.dispatchEvent(e);
|
|
|
|
ShumwayTelemetry.onFallback(userAction);
|
|
}
|
|
|
|
// All the priviledged actions.
|
|
function ChromeActions(url, window, document) {
|
|
this.url = url;
|
|
this.objectParams = null;
|
|
this.movieParams = null;
|
|
this.baseUrl = url;
|
|
this.isOverlay = false;
|
|
this.isPausedAtStart = false;
|
|
this.window = window;
|
|
this.document = document;
|
|
this.externalComInitialized = false;
|
|
this.allowScriptAccess = false;
|
|
this.lastUserInput = 0;
|
|
this.crossdomainRequestsCache = Object.create(null);
|
|
this.telemetry = {
|
|
startTime: Date.now(),
|
|
features: [],
|
|
errors: [],
|
|
pageIndex: 0
|
|
};
|
|
}
|
|
|
|
ChromeActions.prototype = {
|
|
getBoolPref: function (data) {
|
|
if (!/^shumway\./.test(data.pref)) {
|
|
return null;
|
|
}
|
|
return getBoolPref(data.pref, data.def);
|
|
},
|
|
getCompilerSettings: function getCompilerSettings() {
|
|
return {
|
|
appCompiler: getBoolPref('shumway.appCompiler', true),
|
|
sysCompiler: getBoolPref('shumway.sysCompiler', false),
|
|
verifier: getBoolPref('shumway.verifier', true)
|
|
};
|
|
},
|
|
addProfilerMarker: function (marker) {
|
|
if ('nsIProfiler' in Ci) {
|
|
let profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
|
|
profiler.AddMarker(marker);
|
|
}
|
|
},
|
|
getPluginParams: function getPluginParams() {
|
|
return {
|
|
url: this.url,
|
|
baseUrl : this.baseUrl,
|
|
movieParams: this.movieParams,
|
|
objectParams: this.objectParams,
|
|
isOverlay: this.isOverlay,
|
|
isPausedAtStart: this.isPausedAtStart
|
|
};
|
|
},
|
|
_canDownloadFile: function canDownloadFile(data, callback) {
|
|
var url = data.url, checkPolicyFile = data.checkPolicyFile;
|
|
|
|
// TODO flash cross-origin request
|
|
if (url === this.url) {
|
|
// allow downloading for the original file
|
|
return callback({success: true});
|
|
}
|
|
|
|
// allows downloading from the same origin
|
|
var parsedUrl, parsedBaseUrl;
|
|
try {
|
|
parsedUrl = NetUtil.newURI(url);
|
|
} catch (ex) { /* skipping invalid urls */ }
|
|
try {
|
|
parsedBaseUrl = NetUtil.newURI(this.url);
|
|
} catch (ex) { /* skipping invalid urls */ }
|
|
|
|
if (parsedUrl && parsedBaseUrl &&
|
|
parsedUrl.prePath === parsedBaseUrl.prePath) {
|
|
return callback({success: true});
|
|
}
|
|
|
|
// additionally using internal whitelist
|
|
var whitelist = getStringPref('shumway.whitelist', '');
|
|
if (whitelist && parsedUrl) {
|
|
var whitelisted = whitelist.split(',').some(function (i) {
|
|
return domainMatches(parsedUrl.host, i);
|
|
});
|
|
if (whitelisted) {
|
|
return callback({success: true});
|
|
}
|
|
}
|
|
|
|
if (!checkPolicyFile || !parsedUrl || !parsedBaseUrl) {
|
|
return callback({success: false});
|
|
}
|
|
|
|
// we can request crossdomain.xml
|
|
fetchPolicyFile(parsedUrl.prePath + '/crossdomain.xml', this.crossdomainRequestsCache,
|
|
function (policy, error) {
|
|
|
|
if (!policy || policy.siteControl === 'none') {
|
|
return callback({success: false});
|
|
}
|
|
// TODO assuming master-only, there are also 'by-content-type', 'all', etc.
|
|
|
|
var allowed = policy.allowAccessFrom.some(function (i) {
|
|
return domainMatches(parsedBaseUrl.host, i.domain) &&
|
|
(!i.secure || parsedBaseUrl.scheme.toLowerCase() === 'https');
|
|
});
|
|
return callback({success: allowed});
|
|
}.bind(this));
|
|
},
|
|
loadFile: function loadFile(data) {
|
|
function notifyLoadFileListener(data) {
|
|
if (!win.wrappedJSObject.onLoadFileCallback) {
|
|
return;
|
|
}
|
|
win.wrappedJSObject.onLoadFileCallback(data);
|
|
}
|
|
|
|
var url = data.url;
|
|
var checkPolicyFile = data.checkPolicyFile;
|
|
var sessionId = data.sessionId;
|
|
var limit = data.limit || 0;
|
|
var method = data.method || "GET";
|
|
var mimeType = data.mimeType;
|
|
var postData = data.postData || null;
|
|
|
|
var win = this.window;
|
|
var baseUrl = this.baseUrl;
|
|
|
|
var performXHR = function () {
|
|
var xhr = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
|
|
.createInstance(Ci.nsIXMLHttpRequest);
|
|
xhr.open(method, url, true);
|
|
xhr.responseType = "moz-chunked-arraybuffer";
|
|
|
|
if (baseUrl) {
|
|
// Setting the referer uri, some site doing checks if swf is embedded
|
|
// on the original page.
|
|
xhr.setRequestHeader("Referer", baseUrl);
|
|
}
|
|
|
|
// TODO apply range request headers if limit is specified
|
|
|
|
var lastPosition = 0;
|
|
xhr.onprogress = function (e) {
|
|
var position = e.loaded;
|
|
var data = new Uint8Array(xhr.response);
|
|
notifyLoadFileListener({callback:"loadFile", sessionId: sessionId,
|
|
topic: "progress", array: data, loaded: e.loaded, total: e.total});
|
|
lastPosition = position;
|
|
if (limit && e.total >= limit) {
|
|
xhr.abort();
|
|
}
|
|
};
|
|
xhr.onreadystatechange = function(event) {
|
|
if (xhr.readyState === 4) {
|
|
if (xhr.status !== 200 && xhr.status !== 0) {
|
|
notifyLoadFileListener({callback:"loadFile", sessionId: sessionId, topic: "error", error: xhr.statusText});
|
|
}
|
|
notifyLoadFileListener({callback:"loadFile", sessionId: sessionId, topic: "close"});
|
|
}
|
|
};
|
|
if (mimeType)
|
|
xhr.setRequestHeader("Content-Type", mimeType);
|
|
xhr.send(postData);
|
|
notifyLoadFileListener({callback:"loadFile", sessionId: sessionId, topic: "open"});
|
|
};
|
|
|
|
this._canDownloadFile({url: url, checkPolicyFile: checkPolicyFile}, function (data) {
|
|
if (data.success) {
|
|
performXHR();
|
|
} else {
|
|
log("data access id 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"});
|
|
}
|
|
});
|
|
},
|
|
navigateTo: function (data) {
|
|
var embedTag = this.embedTag.wrappedJSObject;
|
|
var window = embedTag ? embedTag.ownerDocument.defaultView : this.window;
|
|
window.open(data.url, data.target || '_self');
|
|
},
|
|
fallback: function(automatic) {
|
|
automatic = !!automatic;
|
|
fallbackToNativePlugin(this.window, !automatic, automatic);
|
|
},
|
|
userInput: function() {
|
|
var win = this.window;
|
|
var winUtils = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
|
|
getInterface(Components.interfaces.nsIDOMWindowUtils);
|
|
if (winUtils.isHandlingUserInput) {
|
|
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) {
|
|
return false;
|
|
}
|
|
// TODO other security checks?
|
|
return true;
|
|
},
|
|
setClipboard: function (data) {
|
|
if (typeof data !== 'string' || !this.isUserInputInProgress()) {
|
|
return;
|
|
}
|
|
|
|
let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
|
|
.getService(Ci.nsIClipboardHelper);
|
|
clipboard.copyString(data);
|
|
},
|
|
setFullscreen: function (enabled) {
|
|
enabled = !!enabled;
|
|
|
|
if (!this.isUserInputInProgress()) {
|
|
return;
|
|
}
|
|
|
|
var target = this.embedTag || this.document.body;
|
|
if (enabled) {
|
|
target.mozRequestFullScreen();
|
|
} else {
|
|
target.ownerDocument.mozCancelFullScreen();
|
|
}
|
|
},
|
|
endActivation: function () {
|
|
if (ActivationQueue.currentNonActive === this) {
|
|
ActivationQueue.activateNext();
|
|
}
|
|
},
|
|
reportTelemetry: function (data) {
|
|
var topic = data.topic;
|
|
switch (topic) {
|
|
case 'firstFrame':
|
|
var time = Date.now() - this.telemetry.startTime;
|
|
ShumwayTelemetry.onFirstFrame(time);
|
|
break;
|
|
case 'parseInfo':
|
|
ShumwayTelemetry.onParseInfo({
|
|
parseTime: +data.parseTime,
|
|
size: +data.bytesTotal,
|
|
swfVersion: data.swfVersion|0,
|
|
frameRate: +data.frameRate,
|
|
width: data.width|0,
|
|
height: data.height|0,
|
|
bannerType: data.bannerType|0,
|
|
isAvm2: !!data.isAvm2
|
|
});
|
|
break;
|
|
case 'feature':
|
|
var featureType = data.feature|0;
|
|
var MIN_FEATURE_TYPE = 0, MAX_FEATURE_TYPE = 999;
|
|
if (featureType >= MIN_FEATURE_TYPE && featureType <= MAX_FEATURE_TYPE &&
|
|
!this.telemetry.features[featureType]) {
|
|
this.telemetry.features[featureType] = true; // record only one feature per SWF
|
|
ShumwayTelemetry.onFeature(featureType);
|
|
}
|
|
break;
|
|
case 'error':
|
|
var errorType = data.error|0;
|
|
var MIN_ERROR_TYPE = 0, MAX_ERROR_TYPE = 2;
|
|
if (errorType >= MIN_ERROR_TYPE && errorType <= MAX_ERROR_TYPE &&
|
|
!this.telemetry.errors[errorType]) {
|
|
this.telemetry.errors[errorType] = true; // record only one report per SWF
|
|
ShumwayTelemetry.onError(errorType);
|
|
}
|
|
break;
|
|
}
|
|
},
|
|
reportIssue: function(exceptions) {
|
|
var urlTemplate = "https://bugzilla.mozilla.org/enter_bug.cgi?op_sys=All&priority=--" +
|
|
"&rep_platform=All&target_milestone=---&version=Trunk&product=Firefox" +
|
|
"&component=Shumway&short_desc=&comment={comment}" +
|
|
"&bug_file_loc={url}";
|
|
var windowUrl = this.window.parent.wrappedJSObject.location + '';
|
|
var url = urlTemplate.split('{url}').join(encodeURIComponent(windowUrl));
|
|
var params = {
|
|
swf: encodeURIComponent(this.url)
|
|
};
|
|
getVersionInfo().then(function (versions) {
|
|
params.versions = versions;
|
|
}).then(function () {
|
|
var ffbuild = params.versions.geckoVersion + ' (' + params.versions.geckoBuildID + ')';
|
|
//params.exceptions = encodeURIComponent(exceptions);
|
|
var comment = '+++ Initially filed via the problem reporting functionality in Shumway +++\n' +
|
|
'Please add any further information that you deem helpful here:\n\n\n\n' +
|
|
'----------------------\n\n' +
|
|
'Technical Information:\n' +
|
|
'Firefox version: ' + ffbuild + '\n' +
|
|
'Shumway version: ' + params.versions.shumwayVersion;
|
|
url = url.split('{comment}').join(encodeURIComponent(comment));
|
|
this.window.open(url);
|
|
}.bind(this));
|
|
},
|
|
externalCom: function (data) {
|
|
if (!this.allowScriptAccess)
|
|
return;
|
|
|
|
// TODO check security ?
|
|
var parentWindow = this.window.parent.wrappedJSObject;
|
|
var embedTag = this.embedTag.wrappedJSObject;
|
|
switch (data.action) {
|
|
case 'init':
|
|
if (this.externalComInitialized)
|
|
return;
|
|
|
|
this.externalComInitialized = true;
|
|
initExternalCom(parentWindow, embedTag, this.window);
|
|
return;
|
|
case 'getId':
|
|
return embedTag.id;
|
|
case 'eval':
|
|
return parentWindow.__flash__eval(data.expression);
|
|
case 'call':
|
|
return parentWindow.__flash__call(data.request);
|
|
case 'register':
|
|
return embedTag.__flash__registerCallback(data.functionName);
|
|
case 'unregister':
|
|
return embedTag.__flash__unregisterCallback(data.functionName);
|
|
}
|
|
},
|
|
getWindowUrl: function() {
|
|
return this.window.parent.wrappedJSObject.location + '';
|
|
}
|
|
};
|
|
|
|
// Event listener to trigger chrome privedged code.
|
|
function RequestListener(actions) {
|
|
this.actions = actions;
|
|
}
|
|
// Receive an event and synchronously or asynchronously responds.
|
|
RequestListener.prototype.receive = function(detail) {
|
|
var action = detail.action;
|
|
var data = detail.data;
|
|
var sync = detail.sync;
|
|
var actions = this.actions;
|
|
if (!(action in actions)) {
|
|
log('Unknown action: ' + action);
|
|
return;
|
|
}
|
|
if (sync) {
|
|
var response = actions[action].call(this.actions, data);
|
|
return response === undefined ? undefined : JSON.stringify(response);
|
|
}
|
|
|
|
var responseCallback;
|
|
if (detail.callback) {
|
|
var cookie = detail.cookie;
|
|
response = function sendResponse(response) {
|
|
var win = actions.window;
|
|
if (win.wrappedJSObject.onMessageCallback) {
|
|
win.wrappedJSObject.onMessageCallback({
|
|
response: response === undefined ? undefined : JSON.stringify(response),
|
|
cookie: cookie
|
|
});
|
|
}
|
|
};
|
|
}
|
|
actions[action].call(this.actions, data, responseCallback);
|
|
};
|
|
|
|
var ActivationQueue = {
|
|
nonActive: [],
|
|
initializing: -1,
|
|
activationTimeout: null,
|
|
get currentNonActive() {
|
|
return this.nonActive[this.initializing];
|
|
},
|
|
enqueue: function ActivationQueue_enqueue(actions) {
|
|
this.nonActive.push(actions);
|
|
if (this.nonActive.length === 1) {
|
|
this.activateNext();
|
|
}
|
|
},
|
|
findLastOnPage: function ActivationQueue_findLastOnPage(baseUrl) {
|
|
for (var i = this.nonActive.length - 1; i >= 0; i--) {
|
|
if (this.nonActive[i].baseUrl === baseUrl) {
|
|
return this.nonActive[i];
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
activateNext: function ActivationQueue_activateNext() {
|
|
function weightInstance(actions) {
|
|
// set of heuristics for find the most important instance to load
|
|
var weight = 0;
|
|
// using linear distance to the top-left of the view area
|
|
if (actions.embedTag) {
|
|
var window = actions.window;
|
|
var clientRect = actions.embedTag.getBoundingClientRect();
|
|
weight -= Math.abs(clientRect.left - window.scrollX) +
|
|
Math.abs(clientRect.top - window.scrollY);
|
|
}
|
|
var doc = actions.document;
|
|
if (!doc.hidden) {
|
|
weight += 100000; // might not be that important if hidden
|
|
}
|
|
if (actions.embedTag &&
|
|
actions.embedTag.ownerDocument.hasFocus()) {
|
|
weight += 10000; // parent document is focused
|
|
}
|
|
return weight;
|
|
}
|
|
|
|
if (this.activationTimeout) {
|
|
this.activationTimeout.cancel();
|
|
this.activationTimeout = null;
|
|
}
|
|
|
|
if (this.initializing >= 0) {
|
|
this.nonActive.splice(this.initializing, 1);
|
|
}
|
|
var weights = [];
|
|
for (var i = 0; i < this.nonActive.length; i++) {
|
|
try {
|
|
var weight = weightInstance(this.nonActive[i]);
|
|
weights.push(weight);
|
|
} catch (ex) {
|
|
// unable to calc weight the instance, removing
|
|
log('Shumway instance weight calculation failed: ' + ex);
|
|
this.nonActive.splice(i, 1);
|
|
i--;
|
|
}
|
|
}
|
|
|
|
do {
|
|
if (this.nonActive.length === 0) {
|
|
this.initializing = -1;
|
|
return;
|
|
}
|
|
|
|
var maxWeightIndex = 0;
|
|
var maxWeight = weights[0];
|
|
for (var i = 1; i < weights.length; i++) {
|
|
if (maxWeight < weights[i]) {
|
|
maxWeight = weights[i];
|
|
maxWeightIndex = i;
|
|
}
|
|
}
|
|
try {
|
|
this.initializing = maxWeightIndex;
|
|
this.nonActive[maxWeightIndex].activationCallback();
|
|
break;
|
|
} catch (ex) {
|
|
// unable to initialize the instance, trying another one
|
|
log('Shumway instance initialization failed: ' + ex);
|
|
this.nonActive.splice(maxWeightIndex, 1);
|
|
weights.splice(maxWeightIndex, 1);
|
|
}
|
|
} while (true);
|
|
|
|
var ACTIVATION_TIMEOUT = 3000;
|
|
this.activationTimeout = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
this.activationTimeout.initWithCallback(function () {
|
|
log('Timeout during shumway instance initialization');
|
|
this.activateNext();
|
|
}.bind(this), ACTIVATION_TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT);
|
|
}
|
|
};
|
|
|
|
function activateShumwayScripts(window, requestListener) {
|
|
function loadScripts(scripts, callback) {
|
|
function loadScript(i) {
|
|
if (i >= scripts.length) {
|
|
callback();
|
|
return;
|
|
}
|
|
var script = document.createElement('script');
|
|
script.type = "text/javascript";
|
|
script.src = scripts[i];
|
|
script.onload = function () {
|
|
loadScript(i + 1);
|
|
};
|
|
head.appendChild(script);
|
|
}
|
|
var document = window.document.wrappedJSObject;
|
|
var head = document.getElementsByTagName('head')[0];
|
|
loadScript(0);
|
|
}
|
|
|
|
function initScripts() {
|
|
window.wrappedJSObject.notifyShumwayMessage = function () {
|
|
return requestListener.receive.apply(requestListener, arguments);
|
|
};
|
|
window.wrappedJSObject.runViewer();
|
|
}
|
|
|
|
if (window.document.readyState === "interactive" ||
|
|
window.document.readyState === "complete") {
|
|
initScripts();
|
|
} else {
|
|
window.document.addEventListener('DOMContentLoaded', initScripts);
|
|
}
|
|
}
|
|
|
|
function initExternalCom(wrappedWindow, wrappedObject, targetWindow) {
|
|
if (!wrappedWindow.__flash__initialized) {
|
|
wrappedWindow.__flash__initialized = true;
|
|
wrappedWindow.__flash__toXML = function __flash__toXML(obj) {
|
|
switch (typeof obj) {
|
|
case 'boolean':
|
|
return obj ? '<true/>' : '<false/>';
|
|
case 'number':
|
|
return '<number>' + obj + '</number>';
|
|
case 'object':
|
|
if (obj === null) {
|
|
return '<null/>';
|
|
}
|
|
if ('hasOwnProperty' in obj && obj.hasOwnProperty('length')) {
|
|
// array
|
|
var xml = '<array>';
|
|
for (var i = 0; i < obj.length; i++) {
|
|
xml += '<property id="' + i + '">' + __flash__toXML(obj[i]) + '</property>';
|
|
}
|
|
return xml + '</array>';
|
|
}
|
|
var xml = '<object>';
|
|
for (var i in obj) {
|
|
xml += '<property id="' + i + '">' + __flash__toXML(obj[i]) + '</property>';
|
|
}
|
|
return xml + '</object>';
|
|
case 'string':
|
|
return '<string>' + obj.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') + '</string>';
|
|
case 'undefined':
|
|
return '<undefined/>';
|
|
}
|
|
};
|
|
wrappedWindow.__flash__eval = function (expr) {
|
|
this.console.log('__flash__eval: ' + expr);
|
|
// allowScriptAccess protects page from unwanted swf scripts,
|
|
// we can execute script in the page context without restrictions.
|
|
return this.eval(expr);
|
|
}.bind(wrappedWindow);
|
|
wrappedWindow.__flash__call = function (expr) {
|
|
this.console.log('__flash__call (ignored): ' + expr);
|
|
};
|
|
}
|
|
wrappedObject.__flash__registerCallback = function (functionName) {
|
|
wrappedWindow.console.log('__flash__registerCallback: ' + functionName);
|
|
Components.utils.exportFunction(function () {
|
|
var args = Array.prototype.slice.call(arguments, 0);
|
|
wrappedWindow.console.log('__flash__callIn: ' + functionName);
|
|
var result;
|
|
if (targetWindow.wrappedJSObject.onExternalCallback) {
|
|
result = targetWindow.wrappedJSObject.onExternalCallback({functionName: functionName, args: args});
|
|
}
|
|
return wrappedWindow.eval(result);
|
|
}, this, { defineAs: functionName });
|
|
};
|
|
wrappedObject.__flash__unregisterCallback = function (functionName) {
|
|
wrappedWindow.console.log('__flash__unregisterCallback: ' + functionName);
|
|
delete this[functionName];
|
|
};
|
|
}
|
|
|
|
function ShumwayStreamConverterBase() {
|
|
}
|
|
|
|
ShumwayStreamConverterBase.prototype = {
|
|
QueryInterface: XPCOMUtils.generateQI([
|
|
Ci.nsISupports,
|
|
Ci.nsIStreamConverter,
|
|
Ci.nsIStreamListener,
|
|
Ci.nsIRequestObserver
|
|
]),
|
|
|
|
/*
|
|
* This component works as such:
|
|
* 1. asyncConvertData stores the listener
|
|
* 2. onStartRequest creates a new channel, streams the viewer and cancels
|
|
* the request so Shumway can do the request
|
|
* Since the request is cancelled onDataAvailable should not be called. The
|
|
* onStopRequest does nothing. The convert function just returns the stream,
|
|
* it's just the synchronous version of asyncConvertData.
|
|
*/
|
|
|
|
// nsIStreamConverter::convert
|
|
convert: function(aFromStream, aFromType, aToType, aCtxt) {
|
|
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
|
},
|
|
|
|
getUrlHint: function(requestUrl) {
|
|
return requestUrl.spec;
|
|
},
|
|
|
|
createChromeActions: function(window, document, urlHint) {
|
|
var url = urlHint;
|
|
var baseUrl;
|
|
var pageUrl;
|
|
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.
|
|
var tagName = element.nodeName, containerElement;
|
|
while (tagName != 'EMBED' && tagName != 'OBJECT') {
|
|
// plugin overlay skipping until the target plugin is found
|
|
isOverlay = true;
|
|
containerElement = element;
|
|
element = element.parentNode;
|
|
if (!element) {
|
|
throw new Error('Plugin element is not found');
|
|
}
|
|
tagName = element.nodeName;
|
|
}
|
|
|
|
if (isOverlay) {
|
|
// HACK For Facebook, CSS embed tag rescaling -- iframe (our overlay)
|
|
// has no styling in document. Shall removed with jsplugins.
|
|
for (var child = window.frameElement; child !== element; child = child.parentNode) {
|
|
child.setAttribute('style', 'max-width: 100%; max-height: 100%');
|
|
}
|
|
|
|
// Checking if overlay is a proper PlayPreview overlay.
|
|
for (var i = 0; i < element.children.length; i++) {
|
|
if (element.children[i] === containerElement) {
|
|
throw new Error('Plugin element is invalid');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!url) { // at this point url shall be known -- asserting
|
|
throw new Error('Movie url is not specified');
|
|
}
|
|
|
|
baseUrl = objectParams.base || pageUrl;
|
|
|
|
var movieParams = {};
|
|
if (objectParams.flashvars) {
|
|
movieParams = parseQueryString(objectParams.flashvars);
|
|
}
|
|
var queryStringMatch = /\?([^#]+)/.exec(url);
|
|
if (queryStringMatch) {
|
|
var queryStringParams = parseQueryString(queryStringMatch[1]);
|
|
for (var i in queryStringParams) {
|
|
if (!(i in movieParams)) {
|
|
movieParams[i] = queryStringParams[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
var allowScriptAccess = false;
|
|
switch (objectParams.allowscriptaccess || 'sameDomain') {
|
|
case 'always':
|
|
allowScriptAccess = true;
|
|
break;
|
|
case 'never':
|
|
allowScriptAccess = false;
|
|
break;
|
|
default:
|
|
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;
|
|
}
|
|
|
|
var actions = new ChromeActions(url, window, document);
|
|
actions.objectParams = objectParams;
|
|
actions.movieParams = movieParams;
|
|
actions.baseUrl = baseUrl || url;
|
|
actions.isOverlay = isOverlay;
|
|
actions.embedTag = element;
|
|
actions.isPausedAtStart = /\bpaused=true$/.test(urlHint);
|
|
actions.allowScriptAccess = allowScriptAccess;
|
|
return actions;
|
|
},
|
|
|
|
// nsIStreamConverter::asyncConvertData
|
|
asyncConvertData: function(aFromType, aToType, aListener, aCtxt) {
|
|
// Store the listener passed to us
|
|
this.listener = aListener;
|
|
},
|
|
|
|
// nsIStreamListener::onDataAvailable
|
|
onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) {
|
|
// Do nothing since all the data loading is handled by the viewer.
|
|
log('SANITY CHECK: onDataAvailable SHOULD NOT BE CALLED!');
|
|
},
|
|
|
|
// nsIRequestObserver::onStartRequest
|
|
onStartRequest: function(aRequest, aContext) {
|
|
// Setup the request so we can use it below.
|
|
aRequest.QueryInterface(Ci.nsIChannel);
|
|
|
|
aRequest.QueryInterface(Ci.nsIWritablePropertyBag);
|
|
|
|
// Change the content type so we don't get stuck in a loop.
|
|
aRequest.setProperty('contentType', aRequest.contentType);
|
|
aRequest.contentType = 'text/html';
|
|
|
|
// TODO For now suspending request, however we can continue fetching data
|
|
aRequest.suspend();
|
|
|
|
var originalURI = aRequest.URI;
|
|
|
|
// Create a new channel that loads the viewer as a chrome resource.
|
|
var viewerUrl = 'chrome://shumway/content/viewer.wrapper.html';
|
|
var channel = Services.io.newChannel(viewerUrl, null, null);
|
|
|
|
var converter = this;
|
|
var listener = this.listener;
|
|
// Proxy all the request observer calls, when it gets to onStopRequest
|
|
// we can get the dom window.
|
|
var proxy = {
|
|
onStartRequest: function(request, context) {
|
|
listener.onStartRequest(aRequest, context);
|
|
},
|
|
onDataAvailable: function(request, context, inputStream, offset, count) {
|
|
listener.onDataAvailable(aRequest, context, inputStream, offset, count);
|
|
},
|
|
onStopRequest: function(request, context, statusCode) {
|
|
// Cancel the request so the viewer can handle it.
|
|
aRequest.resume();
|
|
aRequest.cancel(Cr.NS_BINDING_ABORTED);
|
|
|
|
var domWindow = getDOMWindow(channel);
|
|
let actions = converter.createChromeActions(domWindow,
|
|
domWindow.document,
|
|
converter.getUrlHint(originalURI));
|
|
|
|
if (!isShumwayEnabledFor(actions)) {
|
|
fallbackToNativePlugin(domWindow, false, true);
|
|
return;
|
|
}
|
|
|
|
// Report telemetry on amount of swfs on the page
|
|
if (actions.isOverlay) {
|
|
// Looking for last actions with same baseUrl
|
|
var prevPageActions = ActivationQueue.findLastOnPage(actions.baseUrl);
|
|
var pageIndex = !prevPageActions ? 1 : (prevPageActions.telemetry.pageIndex + 1);
|
|
actions.telemetry.pageIndex = pageIndex;
|
|
ShumwayTelemetry.onPageIndex(pageIndex);
|
|
} else {
|
|
ShumwayTelemetry.onPageIndex(0);
|
|
}
|
|
|
|
let requestListener = new RequestListener(actions);
|
|
|
|
actions.activationCallback = function(domWindow, requestListener) {
|
|
delete this.activationCallback;
|
|
activateShumwayScripts(domWindow, requestListener);
|
|
}.bind(actions, domWindow, requestListener);
|
|
ActivationQueue.enqueue(actions);
|
|
|
|
listener.onStopRequest(aRequest, context, statusCode);
|
|
}
|
|
};
|
|
|
|
// Keep the URL the same so the browser sees it as the same.
|
|
channel.originalURI = aRequest.URI;
|
|
channel.loadGroup = aRequest.loadGroup;
|
|
|
|
// We can use all powerful principal: we are opening chrome:// web page,
|
|
// which will need lots of permission.
|
|
var securityManager = Cc['@mozilla.org/scriptsecuritymanager;1']
|
|
.getService(Ci.nsIScriptSecurityManager);
|
|
var resourcePrincipal = securityManager.getSystemPrincipal();
|
|
aRequest.owner = resourcePrincipal;
|
|
channel.asyncOpen(proxy, aContext);
|
|
},
|
|
|
|
// nsIRequestObserver::onStopRequest
|
|
onStopRequest: function(aRequest, aContext, aStatusCode) {
|
|
// Do nothing.
|
|
}
|
|
};
|
|
|
|
// properties required for XPCOM registration:
|
|
function copyProperties(obj, template) {
|
|
for (var prop in template) {
|
|
obj[prop] = template[prop];
|
|
}
|
|
}
|
|
|
|
function ShumwayStreamConverter() {}
|
|
ShumwayStreamConverter.prototype = new ShumwayStreamConverterBase();
|
|
copyProperties(ShumwayStreamConverter.prototype, {
|
|
classID: Components.ID('{4c6030f7-e20a-264f-5b0e-ada3a9e97384}'),
|
|
classDescription: 'Shumway Content Converter Component',
|
|
contractID: '@mozilla.org/streamconv;1?from=application/x-shockwave-flash&to=*/*'
|
|
});
|
|
|
|
function ShumwayStreamOverlayConverter() {}
|
|
ShumwayStreamOverlayConverter.prototype = new ShumwayStreamConverterBase();
|
|
copyProperties(ShumwayStreamOverlayConverter.prototype, {
|
|
classID: Components.ID('{4c6030f7-e20a-264f-5f9b-ada3a9e97384}'),
|
|
classDescription: 'Shumway PlayPreview Component',
|
|
contractID: '@mozilla.org/streamconv;1?from=application/x-moz-playpreview&to=*/*'
|
|
});
|
|
ShumwayStreamOverlayConverter.prototype.getUrlHint = function (requestUrl) {
|
|
return '';
|
|
};
|