michael@0: /* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */
michael@0: /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
michael@0: /*
michael@0: * Copyright 2013 Mozilla Foundation
michael@0: *
michael@0: * Licensed under the Apache License, Version 2.0 (the "License");
michael@0: * you may not use this file except in compliance with the License.
michael@0: * You may obtain a copy of the License at
michael@0: *
michael@0: * http://www.apache.org/licenses/LICENSE-2.0
michael@0: *
michael@0: * Unless required by applicable law or agreed to in writing, software
michael@0: * distributed under the License is distributed on an "AS IS" BASIS,
michael@0: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
michael@0: * See the License for the specific language governing permissions and
michael@0: * limitations under the License.
michael@0: */
michael@0:
michael@0: 'use strict';
michael@0:
michael@0: var EXPORTED_SYMBOLS = ['ShumwayStreamConverter', 'ShumwayStreamOverlayConverter'];
michael@0:
michael@0: const Cc = Components.classes;
michael@0: const Ci = Components.interfaces;
michael@0: const Cr = Components.results;
michael@0: const Cu = Components.utils;
michael@0:
michael@0: const SHUMWAY_CONTENT_TYPE = 'application/x-shockwave-flash';
michael@0: const EXPECTED_PLAYPREVIEW_URI_PREFIX = 'data:application/x-moz-playpreview;,' +
michael@0: SHUMWAY_CONTENT_TYPE;
michael@0:
michael@0: const FIREFOX_ID = '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}';
michael@0: const SEAMONKEY_ID = '{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}';
michael@0:
michael@0: const MAX_CLIPBOARD_DATA_SIZE = 8000;
michael@0:
michael@0: Cu.import('resource://gre/modules/XPCOMUtils.jsm');
michael@0: Cu.import('resource://gre/modules/Services.jsm');
michael@0: Cu.import('resource://gre/modules/NetUtil.jsm');
michael@0: Cu.import('resource://gre/modules/Promise.jsm');
michael@0:
michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'PrivateBrowsingUtils',
michael@0: 'resource://gre/modules/PrivateBrowsingUtils.jsm');
michael@0:
michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'AddonManager',
michael@0: 'resource://gre/modules/AddonManager.jsm');
michael@0:
michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'ShumwayTelemetry',
michael@0: 'resource://shumway/ShumwayTelemetry.jsm');
michael@0:
michael@0: let Svc = {};
michael@0: XPCOMUtils.defineLazyServiceGetter(Svc, 'mime',
michael@0: '@mozilla.org/mime;1', 'nsIMIMEService');
michael@0:
michael@0: let StringInputStream = Cc["@mozilla.org/io/string-input-stream;1"];
michael@0: let MimeInputStream = Cc["@mozilla.org/network/mime-input-stream;1"];
michael@0:
michael@0: function getBoolPref(pref, def) {
michael@0: try {
michael@0: return Services.prefs.getBoolPref(pref);
michael@0: } catch (ex) {
michael@0: return def;
michael@0: }
michael@0: }
michael@0:
michael@0: function getStringPref(pref, def) {
michael@0: try {
michael@0: return Services.prefs.getComplexValue(pref, Ci.nsISupportsString).data;
michael@0: } catch (ex) {
michael@0: return def;
michael@0: }
michael@0: }
michael@0:
michael@0: function log(aMsg) {
michael@0: let msg = 'ShumwayStreamConverter.js: ' + (aMsg.join ? aMsg.join('') : aMsg);
michael@0: Services.console.logStringMessage(msg);
michael@0: dump(msg + '\n');
michael@0: }
michael@0:
michael@0: function getDOMWindow(aChannel) {
michael@0: var requestor = aChannel.notificationCallbacks ||
michael@0: aChannel.loadGroup.notificationCallbacks;
michael@0: var win = requestor.getInterface(Components.interfaces.nsIDOMWindow);
michael@0: return win;
michael@0: }
michael@0:
michael@0: function parseQueryString(qs) {
michael@0: if (!qs)
michael@0: return {};
michael@0:
michael@0: if (qs.charAt(0) == '?')
michael@0: qs = qs.slice(1);
michael@0:
michael@0: var values = qs.split('&');
michael@0: var obj = {};
michael@0: for (var i = 0; i < values.length; i++) {
michael@0: var kv = values[i].split('=');
michael@0: var key = kv[0], value = kv[1];
michael@0: obj[decodeURIComponent(key)] = decodeURIComponent(value);
michael@0: }
michael@0:
michael@0: return obj;
michael@0: }
michael@0:
michael@0: function domainMatches(host, pattern) {
michael@0: if (!pattern) return false;
michael@0: if (pattern === '*') return true;
michael@0: host = host.toLowerCase();
michael@0: var parts = pattern.toLowerCase().split('*');
michael@0: if (host.indexOf(parts[0]) !== 0) return false;
michael@0: var p = parts[0].length;
michael@0: for (var i = 1; i < parts.length; i++) {
michael@0: var j = host.indexOf(parts[i], p);
michael@0: if (j === -1) return false;
michael@0: p = j + parts[i].length;
michael@0: }
michael@0: return parts[parts.length - 1] === '' || p === host.length;
michael@0: }
michael@0:
michael@0: function fetchPolicyFile(url, cache, callback) {
michael@0: if (url in cache) {
michael@0: return callback(cache[url]);
michael@0: }
michael@0:
michael@0: log('Fetching policy file at ' + url);
michael@0: var MAX_POLICY_SIZE = 8192;
michael@0: var xhr = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
michael@0: .createInstance(Ci.nsIXMLHttpRequest);
michael@0: xhr.open('GET', url, true);
michael@0: xhr.overrideMimeType('text/xml');
michael@0: xhr.onprogress = function (e) {
michael@0: if (e.loaded >= MAX_POLICY_SIZE) {
michael@0: xhr.abort();
michael@0: cache[url] = false;
michael@0: callback(null, 'Max policy size');
michael@0: }
michael@0: };
michael@0: xhr.onreadystatechange = function(event) {
michael@0: if (xhr.readyState === 4) {
michael@0: // TODO disable redirects
michael@0: var doc = xhr.responseXML;
michael@0: if (xhr.status !== 200 || !doc) {
michael@0: cache[url] = false;
michael@0: return callback(null, 'Invalid HTTP status: ' + xhr.statusText);
michael@0: }
michael@0: // parsing params
michael@0: var params = doc.documentElement.childNodes;
michael@0: var policy = { siteControl: null, allowAccessFrom: []};
michael@0: for (var i = 0; i < params.length; i++) {
michael@0: switch (params[i].localName) {
michael@0: case 'site-control':
michael@0: policy.siteControl = params[i].getAttribute('permitted-cross-domain-policies');
michael@0: break;
michael@0: case 'allow-access-from':
michael@0: var access = {
michael@0: domain: params[i].getAttribute('domain'),
michael@0: security: params[i].getAttribute('security') === 'true'
michael@0: };
michael@0: policy.allowAccessFrom.push(access);
michael@0: break;
michael@0: default:
michael@0: // TODO allow-http-request-headers-from and other
michael@0: break;
michael@0: }
michael@0: }
michael@0: callback(cache[url] = policy);
michael@0: }
michael@0: };
michael@0: xhr.send(null);
michael@0: }
michael@0:
michael@0: function isShumwayEnabledFor(actions) {
michael@0: // disabled for PrivateBrowsing windows
michael@0: if (PrivateBrowsingUtils.isWindowPrivate(actions.window)) {
michael@0: return false;
michael@0: }
michael@0: // disabled if embed tag specifies shumwaymode (for testing purpose)
michael@0: if (actions.objectParams['shumwaymode'] === 'off') {
michael@0: return false;
michael@0: }
michael@0:
michael@0: var url = actions.url;
michael@0: var baseUrl = actions.baseUrl;
michael@0:
michael@0: // blacklisting well known sites with issues
michael@0: if (/\.ytimg\.com\//i.test(url) /* youtube movies */ ||
michael@0: /\/vui.swf\b/i.test(url) /* vidyo manager */ ||
michael@0: /soundcloud\.com\/player\/assets\/swf/i.test(url) /* soundcloud */ ||
michael@0: /sndcdn\.com\/assets\/swf/.test(url) /* soundcloud */ ||
michael@0: /vimeocdn\.com/.test(url) /* vimeo */) {
michael@0: return false;
michael@0: }
michael@0:
michael@0: return true;
michael@0: }
michael@0:
michael@0: function getVersionInfo() {
michael@0: var deferred = Promise.defer();
michael@0: var versionInfo = {
michael@0: geckoMstone : 'unknown',
michael@0: geckoBuildID: 'unknown',
michael@0: shumwayVersion: 'unknown'
michael@0: };
michael@0: try {
michael@0: versionInfo.geckoMstone = Services.prefs.getCharPref('gecko.mstone');
michael@0: versionInfo.geckoBuildID = Services.prefs.getCharPref('gecko.buildID');
michael@0: } catch (e) {
michael@0: log('Error encountered while getting platform version info:', e);
michael@0: }
michael@0: try {
michael@0: var addonId = "shumway@research.mozilla.org";
michael@0: AddonManager.getAddonByID(addonId, function(addon) {
michael@0: versionInfo.shumwayVersion = addon ? addon.version : 'n/a';
michael@0: deferred.resolve(versionInfo);
michael@0: });
michael@0: } catch (e) {
michael@0: log('Error encountered while getting Shumway version info:', e);
michael@0: deferred.resolve(versionInfo);
michael@0: }
michael@0: return deferred.promise;
michael@0: }
michael@0:
michael@0: function fallbackToNativePlugin(window, userAction, activateCTP) {
michael@0: var obj = window.frameElement;
michael@0: var doc = obj.ownerDocument;
michael@0: var e = doc.createEvent("CustomEvent");
michael@0: e.initCustomEvent("MozPlayPlugin", true, true, activateCTP);
michael@0: obj.dispatchEvent(e);
michael@0:
michael@0: ShumwayTelemetry.onFallback(userAction);
michael@0: }
michael@0:
michael@0: // All the priviledged actions.
michael@0: function ChromeActions(url, window, document) {
michael@0: this.url = url;
michael@0: this.objectParams = null;
michael@0: this.movieParams = null;
michael@0: this.baseUrl = url;
michael@0: this.isOverlay = false;
michael@0: this.isPausedAtStart = false;
michael@0: this.window = window;
michael@0: this.document = document;
michael@0: this.externalComInitialized = false;
michael@0: this.allowScriptAccess = false;
michael@0: this.crossdomainRequestsCache = Object.create(null);
michael@0: this.telemetry = {
michael@0: startTime: Date.now(),
michael@0: features: [],
michael@0: errors: [],
michael@0: pageIndex: 0
michael@0: };
michael@0: }
michael@0:
michael@0: ChromeActions.prototype = {
michael@0: getBoolPref: function (data) {
michael@0: if (!/^shumway\./.test(data.pref)) {
michael@0: return null;
michael@0: }
michael@0: return getBoolPref(data.pref, data.def);
michael@0: },
michael@0: getCompilerSettings: function getCompilerSettings() {
michael@0: return JSON.stringify({
michael@0: appCompiler: getBoolPref('shumway.appCompiler', true),
michael@0: sysCompiler: getBoolPref('shumway.sysCompiler', false),
michael@0: verifier: getBoolPref('shumway.verifier', true)
michael@0: });
michael@0: },
michael@0: addProfilerMarker: function (marker) {
michael@0: if ('nsIProfiler' in Ci) {
michael@0: let profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
michael@0: profiler.AddMarker(marker);
michael@0: }
michael@0: },
michael@0: getPluginParams: function getPluginParams() {
michael@0: return JSON.stringify({
michael@0: url: this.url,
michael@0: baseUrl : this.baseUrl,
michael@0: movieParams: this.movieParams,
michael@0: objectParams: this.objectParams,
michael@0: isOverlay: this.isOverlay,
michael@0: isPausedAtStart: this.isPausedAtStart
michael@0: });
michael@0: },
michael@0: _canDownloadFile: function canDownloadFile(data, callback) {
michael@0: var url = data.url, checkPolicyFile = data.checkPolicyFile;
michael@0:
michael@0: // TODO flash cross-origin request
michael@0: if (url === this.url) {
michael@0: // allow downloading for the original file
michael@0: return callback({success: true});
michael@0: }
michael@0:
michael@0: // allows downloading from the same origin
michael@0: var parsedUrl, parsedBaseUrl;
michael@0: try {
michael@0: parsedUrl = NetUtil.newURI(url);
michael@0: } catch (ex) { /* skipping invalid urls */ }
michael@0: try {
michael@0: parsedBaseUrl = NetUtil.newURI(this.url);
michael@0: } catch (ex) { /* skipping invalid urls */ }
michael@0:
michael@0: if (parsedUrl && parsedBaseUrl &&
michael@0: parsedUrl.prePath === parsedBaseUrl.prePath) {
michael@0: return callback({success: true});
michael@0: }
michael@0:
michael@0: // additionally using internal whitelist
michael@0: var whitelist = getStringPref('shumway.whitelist', '');
michael@0: if (whitelist && parsedUrl) {
michael@0: var whitelisted = whitelist.split(',').some(function (i) {
michael@0: return domainMatches(parsedUrl.host, i);
michael@0: });
michael@0: if (whitelisted) {
michael@0: return callback({success: true});
michael@0: }
michael@0: }
michael@0:
michael@0: if (!checkPolicyFile || !parsedUrl || !parsedBaseUrl) {
michael@0: return callback({success: false});
michael@0: }
michael@0:
michael@0: // we can request crossdomain.xml
michael@0: fetchPolicyFile(parsedUrl.prePath + '/crossdomain.xml', this.crossdomainRequestsCache,
michael@0: function (policy, error) {
michael@0:
michael@0: if (!policy || policy.siteControl === 'none') {
michael@0: return callback({success: false});
michael@0: }
michael@0: // TODO assuming master-only, there are also 'by-content-type', 'all', etc.
michael@0:
michael@0: var allowed = policy.allowAccessFrom.some(function (i) {
michael@0: return domainMatches(parsedBaseUrl.host, i.domain) &&
michael@0: (!i.secure || parsedBaseUrl.scheme.toLowerCase() === 'https');
michael@0: });
michael@0: return callback({success: allowed});
michael@0: }.bind(this));
michael@0: },
michael@0: loadFile: function loadFile(data) {
michael@0: var url = data.url;
michael@0: var checkPolicyFile = data.checkPolicyFile;
michael@0: var sessionId = data.sessionId;
michael@0: var limit = data.limit || 0;
michael@0: var method = data.method || "GET";
michael@0: var mimeType = data.mimeType;
michael@0: var postData = data.postData || null;
michael@0:
michael@0: var win = this.window;
michael@0: var baseUrl = this.baseUrl;
michael@0:
michael@0: var performXHR = function () {
michael@0: var xhr = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
michael@0: .createInstance(Ci.nsIXMLHttpRequest);
michael@0: xhr.open(method, url, true);
michael@0: xhr.responseType = "moz-chunked-arraybuffer";
michael@0:
michael@0: if (baseUrl) {
michael@0: // Setting the referer uri, some site doing checks if swf is embedded
michael@0: // on the original page.
michael@0: xhr.setRequestHeader("Referer", baseUrl);
michael@0: }
michael@0:
michael@0: // TODO apply range request headers if limit is specified
michael@0:
michael@0: var lastPosition = 0;
michael@0: xhr.onprogress = function (e) {
michael@0: var position = e.loaded;
michael@0: var data = new Uint8Array(xhr.response);
michael@0: win.postMessage({callback:"loadFile", sessionId: sessionId, topic: "progress",
michael@0: array: data, loaded: e.loaded, total: e.total}, "*");
michael@0: lastPosition = position;
michael@0: if (limit && e.total >= limit) {
michael@0: xhr.abort();
michael@0: }
michael@0: };
michael@0: xhr.onreadystatechange = function(event) {
michael@0: if (xhr.readyState === 4) {
michael@0: if (xhr.status !== 200 && xhr.status !== 0) {
michael@0: win.postMessage({callback:"loadFile", sessionId: sessionId, topic: "error",
michael@0: error: xhr.statusText}, "*");
michael@0: }
michael@0: win.postMessage({callback:"loadFile", sessionId: sessionId, topic: "close"}, "*");
michael@0: }
michael@0: };
michael@0: if (mimeType)
michael@0: xhr.setRequestHeader("Content-Type", mimeType);
michael@0: xhr.send(postData);
michael@0: win.postMessage({callback:"loadFile", sessionId: sessionId, topic: "open"}, "*");
michael@0: };
michael@0:
michael@0: this._canDownloadFile({url: url, checkPolicyFile: checkPolicyFile}, function (data) {
michael@0: if (data.success) {
michael@0: performXHR();
michael@0: } else {
michael@0: log("data access id prohibited to " + url + " from " + baseUrl);
michael@0: win.postMessage({callback:"loadFile", sessionId: sessionId, topic: "error",
michael@0: error: "only original swf file or file from the same origin loading supported"}, "*");
michael@0: }
michael@0: });
michael@0: },
michael@0: fallback: function(automatic) {
michael@0: automatic = !!automatic;
michael@0: fallbackToNativePlugin(this.window, !automatic, automatic);
michael@0: },
michael@0: setClipboard: function (data) {
michael@0: if (typeof data !== 'string' ||
michael@0: data.length > MAX_CLIPBOARD_DATA_SIZE ||
michael@0: !this.document.hasFocus()) {
michael@0: return;
michael@0: }
michael@0: // TODO other security checks?
michael@0:
michael@0: let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
michael@0: .getService(Ci.nsIClipboardHelper);
michael@0: clipboard.copyString(data);
michael@0: },
michael@0: unsafeSetClipboard: function (data) {
michael@0: if (typeof data !== 'string') {
michael@0: return;
michael@0: }
michael@0: let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
michael@0: clipboard.copyString(data);
michael@0: },
michael@0: endActivation: function () {
michael@0: if (ActivationQueue.currentNonActive === this) {
michael@0: ActivationQueue.activateNext();
michael@0: }
michael@0: },
michael@0: reportTelemetry: function (data) {
michael@0: var topic = data.topic;
michael@0: switch (topic) {
michael@0: case 'firstFrame':
michael@0: var time = Date.now() - this.telemetry.startTime;
michael@0: ShumwayTelemetry.onFirstFrame(time);
michael@0: break;
michael@0: case 'parseInfo':
michael@0: ShumwayTelemetry.onParseInfo({
michael@0: parseTime: +data.parseTime,
michael@0: size: +data.bytesTotal,
michael@0: swfVersion: data.swfVersion|0,
michael@0: frameRate: +data.frameRate,
michael@0: width: data.width|0,
michael@0: height: data.height|0,
michael@0: bannerType: data.bannerType|0,
michael@0: isAvm2: !!data.isAvm2
michael@0: });
michael@0: break;
michael@0: case 'feature':
michael@0: var featureType = data.feature|0;
michael@0: var MIN_FEATURE_TYPE = 0, MAX_FEATURE_TYPE = 999;
michael@0: if (featureType >= MIN_FEATURE_TYPE && featureType <= MAX_FEATURE_TYPE &&
michael@0: !this.telemetry.features[featureType]) {
michael@0: this.telemetry.features[featureType] = true; // record only one feature per SWF
michael@0: ShumwayTelemetry.onFeature(featureType);
michael@0: }
michael@0: break;
michael@0: case 'error':
michael@0: var errorType = data.error|0;
michael@0: var MIN_ERROR_TYPE = 0, MAX_ERROR_TYPE = 2;
michael@0: if (errorType >= MIN_ERROR_TYPE && errorType <= MAX_ERROR_TYPE &&
michael@0: !this.telemetry.errors[errorType]) {
michael@0: this.telemetry.errors[errorType] = true; // record only one report per SWF
michael@0: ShumwayTelemetry.onError(errorType);
michael@0: }
michael@0: break;
michael@0: }
michael@0: },
michael@0: reportIssue: function(exceptions) {
michael@0: var base = "http://shumway-issue-reporter.paas.allizom.org/input?";
michael@0: var windowUrl = this.window.parent.wrappedJSObject.location + '';
michael@0: var params = 'url=' + encodeURIComponent(windowUrl);
michael@0: params += '&swf=' + encodeURIComponent(this.url);
michael@0: getVersionInfo().then(function (versions) {
michael@0: params += '&ffbuild=' + encodeURIComponent(versions.geckoMstone + ' (' +
michael@0: versions.geckoBuildID + ')');
michael@0: params += '&shubuild=' + encodeURIComponent(versions.shumwayVersion);
michael@0: }).then(function () {
michael@0: var postDataStream = StringInputStream.
michael@0: createInstance(Ci.nsIStringInputStream);
michael@0: postDataStream.data = 'exceptions=' + encodeURIComponent(exceptions);
michael@0: var postData = MimeInputStream.createInstance(Ci.nsIMIMEInputStream);
michael@0: postData.addHeader("Content-Type", "application/x-www-form-urlencoded");
michael@0: postData.addContentLength = true;
michael@0: postData.setData(postDataStream);
michael@0: this.window.openDialog('chrome://browser/content', '_blank',
michael@0: 'all,dialog=no', base + params, null, null,
michael@0: postData);
michael@0: }.bind(this));
michael@0: },
michael@0: externalCom: function (data) {
michael@0: if (!this.allowScriptAccess)
michael@0: return;
michael@0:
michael@0: // TODO check security ?
michael@0: var parentWindow = this.window.parent.wrappedJSObject;
michael@0: var embedTag = this.embedTag.wrappedJSObject;
michael@0: switch (data.action) {
michael@0: case 'init':
michael@0: if (this.externalComInitialized)
michael@0: return;
michael@0:
michael@0: this.externalComInitialized = true;
michael@0: var eventTarget = this.window.document;
michael@0: initExternalCom(parentWindow, embedTag, eventTarget);
michael@0: return;
michael@0: case 'getId':
michael@0: return embedTag.id;
michael@0: case 'eval':
michael@0: return parentWindow.__flash__eval(data.expression);
michael@0: case 'call':
michael@0: return parentWindow.__flash__call(data.request);
michael@0: case 'register':
michael@0: return embedTag.__flash__registerCallback(data.functionName);
michael@0: case 'unregister':
michael@0: return embedTag.__flash__unregisterCallback(data.functionName);
michael@0: }
michael@0: },
michael@0: getWindowUrl: function() {
michael@0: return this.window.parent.wrappedJSObject.location + '';
michael@0: }
michael@0: };
michael@0:
michael@0: // Event listener to trigger chrome privedged code.
michael@0: function RequestListener(actions) {
michael@0: this.actions = actions;
michael@0: }
michael@0: // Receive an event and synchronously or asynchronously responds.
michael@0: RequestListener.prototype.receive = function(event) {
michael@0: var message = event.target;
michael@0: var action = event.detail.action;
michael@0: var data = event.detail.data;
michael@0: var sync = event.detail.sync;
michael@0: var actions = this.actions;
michael@0: if (!(action in actions)) {
michael@0: log('Unknown action: ' + action);
michael@0: return;
michael@0: }
michael@0: if (sync) {
michael@0: var response = actions[action].call(this.actions, data);
michael@0: var detail = event.detail;
michael@0: detail.__exposedProps__ = {response: 'r'};
michael@0: detail.response = response;
michael@0: } else {
michael@0: var response;
michael@0: if (event.detail.callback) {
michael@0: var cookie = event.detail.cookie;
michael@0: response = function sendResponse(response) {
michael@0: var doc = actions.document;
michael@0: try {
michael@0: var listener = doc.createEvent('CustomEvent');
michael@0: listener.initCustomEvent('shumway.response', true, false,
michael@0: {response: response,
michael@0: cookie: cookie,
michael@0: __exposedProps__: {response: 'r', cookie: 'r'}});
michael@0:
michael@0: return message.dispatchEvent(listener);
michael@0: } catch (e) {
michael@0: // doc is no longer accessible because the requestor is already
michael@0: // gone. unloaded content cannot receive the response anyway.
michael@0: }
michael@0: };
michael@0: }
michael@0: actions[action].call(this.actions, data, response);
michael@0: }
michael@0: };
michael@0:
michael@0: var ActivationQueue = {
michael@0: nonActive: [],
michael@0: initializing: -1,
michael@0: activationTimeout: null,
michael@0: get currentNonActive() {
michael@0: return this.nonActive[this.initializing];
michael@0: },
michael@0: enqueue: function ActivationQueue_enqueue(actions) {
michael@0: this.nonActive.push(actions);
michael@0: if (this.nonActive.length === 1) {
michael@0: this.activateNext();
michael@0: }
michael@0: },
michael@0: findLastOnPage: function ActivationQueue_findLastOnPage(baseUrl) {
michael@0: for (var i = this.nonActive.length - 1; i >= 0; i--) {
michael@0: if (this.nonActive[i].baseUrl === baseUrl) {
michael@0: return this.nonActive[i];
michael@0: }
michael@0: }
michael@0: return null;
michael@0: },
michael@0: activateNext: function ActivationQueue_activateNext() {
michael@0: function weightInstance(actions) {
michael@0: // set of heuristics for find the most important instance to load
michael@0: var weight = 0;
michael@0: // using linear distance to the top-left of the view area
michael@0: if (actions.embedTag) {
michael@0: var window = actions.window;
michael@0: var clientRect = actions.embedTag.getBoundingClientRect();
michael@0: weight -= Math.abs(clientRect.left - window.scrollX) +
michael@0: Math.abs(clientRect.top - window.scrollY);
michael@0: }
michael@0: var doc = actions.document;
michael@0: if (!doc.hidden) {
michael@0: weight += 100000; // might not be that important if hidden
michael@0: }
michael@0: if (actions.embedTag &&
michael@0: actions.embedTag.ownerDocument.hasFocus()) {
michael@0: weight += 10000; // parent document is focused
michael@0: }
michael@0: return weight;
michael@0: }
michael@0:
michael@0: if (this.activationTimeout) {
michael@0: this.activationTimeout.cancel();
michael@0: this.activationTimeout = null;
michael@0: }
michael@0:
michael@0: if (this.initializing >= 0) {
michael@0: this.nonActive.splice(this.initializing, 1);
michael@0: }
michael@0: var weights = [];
michael@0: for (var i = 0; i < this.nonActive.length; i++) {
michael@0: try {
michael@0: var weight = weightInstance(this.nonActive[i]);
michael@0: weights.push(weight);
michael@0: } catch (ex) {
michael@0: // unable to calc weight the instance, removing
michael@0: log('Shumway instance weight calculation failed: ' + ex);
michael@0: this.nonActive.splice(i, 1);
michael@0: i--;
michael@0: }
michael@0: }
michael@0:
michael@0: do {
michael@0: if (this.nonActive.length === 0) {
michael@0: this.initializing = -1;
michael@0: return;
michael@0: }
michael@0:
michael@0: var maxWeightIndex = 0;
michael@0: var maxWeight = weights[0];
michael@0: for (var i = 1; i < weights.length; i++) {
michael@0: if (maxWeight < weights[i]) {
michael@0: maxWeight = weights[i];
michael@0: maxWeightIndex = i;
michael@0: }
michael@0: }
michael@0: try {
michael@0: this.initializing = maxWeightIndex;
michael@0: this.nonActive[maxWeightIndex].activationCallback();
michael@0: break;
michael@0: } catch (ex) {
michael@0: // unable to initialize the instance, trying another one
michael@0: log('Shumway instance initialization failed: ' + ex);
michael@0: this.nonActive.splice(maxWeightIndex, 1);
michael@0: weights.splice(maxWeightIndex, 1);
michael@0: }
michael@0: } while (true);
michael@0:
michael@0: var ACTIVATION_TIMEOUT = 3000;
michael@0: this.activationTimeout = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
michael@0: this.activationTimeout.initWithCallback(function () {
michael@0: log('Timeout during shumway instance initialization');
michael@0: this.activateNext();
michael@0: }.bind(this), ACTIVATION_TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT);
michael@0: }
michael@0: };
michael@0:
michael@0: function activateShumwayScripts(window, preview) {
michael@0: function loadScripts(scripts, callback) {
michael@0: function scriptLoaded() {
michael@0: leftToLoad--;
michael@0: if (leftToLoad === 0) {
michael@0: callback();
michael@0: }
michael@0: }
michael@0: var leftToLoad = scripts.length;
michael@0: var document = window.document.wrappedJSObject;
michael@0: var head = document.getElementsByTagName('head')[0];
michael@0: for (var i = 0; i < scripts.length; i++) {
michael@0: var script = document.createElement('script');
michael@0: script.type = "text/javascript";
michael@0: script.src = scripts[i];
michael@0: script.onload = scriptLoaded;
michael@0: head.appendChild(script);
michael@0: }
michael@0: }
michael@0:
michael@0: function initScripts() {
michael@0: if (preview) {
michael@0: loadScripts(['resource://shumway/web/preview.js'], function () {
michael@0: window.wrappedJSObject.runSniffer();
michael@0: });
michael@0: } else {
michael@0: loadScripts(['resource://shumway/shumway.js',
michael@0: 'resource://shumway/web/avm-sandbox.js'], function () {
michael@0: window.wrappedJSObject.runViewer();
michael@0: });
michael@0: }
michael@0: }
michael@0:
michael@0: window.wrappedJSObject.SHUMWAY_ROOT = "resource://shumway/";
michael@0:
michael@0: if (window.document.readyState === "interactive" ||
michael@0: window.document.readyState === "complete") {
michael@0: initScripts();
michael@0: } else {
michael@0: window.document.addEventListener('DOMContentLoaded', initScripts);
michael@0: }
michael@0: }
michael@0:
michael@0: function initExternalCom(wrappedWindow, wrappedObject, targetDocument) {
michael@0: if (!wrappedWindow.__flash__initialized) {
michael@0: wrappedWindow.__flash__initialized = true;
michael@0: wrappedWindow.__flash__toXML = function __flash__toXML(obj) {
michael@0: switch (typeof obj) {
michael@0: case 'boolean':
michael@0: return obj ? '' : '';
michael@0: case 'number':
michael@0: return '' + obj + '';
michael@0: case 'object':
michael@0: if (obj === null) {
michael@0: return '';
michael@0: }
michael@0: if ('hasOwnProperty' in obj && obj.hasOwnProperty('length')) {
michael@0: // array
michael@0: var xml = '';
michael@0: for (var i = 0; i < obj.length; i++) {
michael@0: xml += '' + __flash__toXML(obj[i]) + '';
michael@0: }
michael@0: return xml + '';
michael@0: }
michael@0: var xml = '';
michael@0: case 'string':
michael@0: return '' + obj.replace(/&/g, '&').replace(//g, '>') + '';
michael@0: case 'undefined':
michael@0: return '';
michael@0: }
michael@0: };
michael@0: var sandbox = new Cu.Sandbox(wrappedWindow, {sandboxPrototype: wrappedWindow});
michael@0: wrappedWindow.__flash__eval = function (evalInSandbox, sandbox, expr) {
michael@0: this.console.log('__flash__eval: ' + expr);
michael@0: return evalInSandbox(expr, sandbox);
michael@0: }.bind(wrappedWindow, Cu.evalInSandbox, sandbox);
michael@0: wrappedWindow.__flash__call = function (expr) {
michael@0: this.console.log('__flash__call (ignored): ' + expr);
michael@0: };
michael@0: }
michael@0: wrappedObject.__flash__registerCallback = function (functionName) {
michael@0: wrappedWindow.console.log('__flash__registerCallback: ' + functionName);
michael@0: this[functionName] = function () {
michael@0: var args = Array.prototype.slice.call(arguments, 0);
michael@0: wrappedWindow.console.log('__flash__callIn: ' + functionName);
michael@0: var e = targetDocument.createEvent('CustomEvent');
michael@0: e.initCustomEvent('shumway.remote', true, false, {
michael@0: functionName: functionName,
michael@0: args: args,
michael@0: __exposedProps__: {args: 'r', functionName: 'r', result: 'rw'}
michael@0: });
michael@0: targetDocument.dispatchEvent(e);
michael@0: return e.detail.result;
michael@0: };
michael@0: };
michael@0: wrappedObject.__flash__unregisterCallback = function (functionName) {
michael@0: wrappedWindow.console.log('__flash__unregisterCallback: ' + functionName);
michael@0: delete this[functionName];
michael@0: };
michael@0: }
michael@0:
michael@0: function ShumwayStreamConverterBase() {
michael@0: }
michael@0:
michael@0: ShumwayStreamConverterBase.prototype = {
michael@0: QueryInterface: XPCOMUtils.generateQI([
michael@0: Ci.nsISupports,
michael@0: Ci.nsIStreamConverter,
michael@0: Ci.nsIStreamListener,
michael@0: Ci.nsIRequestObserver
michael@0: ]),
michael@0:
michael@0: /*
michael@0: * This component works as such:
michael@0: * 1. asyncConvertData stores the listener
michael@0: * 2. onStartRequest creates a new channel, streams the viewer and cancels
michael@0: * the request so Shumway can do the request
michael@0: * Since the request is cancelled onDataAvailable should not be called. The
michael@0: * onStopRequest does nothing. The convert function just returns the stream,
michael@0: * it's just the synchronous version of asyncConvertData.
michael@0: */
michael@0:
michael@0: // nsIStreamConverter::convert
michael@0: convert: function(aFromStream, aFromType, aToType, aCtxt) {
michael@0: throw Cr.NS_ERROR_NOT_IMPLEMENTED;
michael@0: },
michael@0:
michael@0: getUrlHint: function(requestUrl) {
michael@0: return requestUrl.spec;
michael@0: },
michael@0:
michael@0: createChromeActions: function(window, document, urlHint) {
michael@0: var url = urlHint;
michael@0: var baseUrl;
michael@0: var pageUrl;
michael@0: var element = window.frameElement;
michael@0: var isOverlay = false;
michael@0: var objectParams = {};
michael@0: if (element) {
michael@0: // PlayPreview overlay "belongs" to the embed/object tag and consists of
michael@0: // DIV and IFRAME. Starting from IFRAME and looking for first object tag.
michael@0: var tagName = element.nodeName, containerElement;
michael@0: while (tagName != 'EMBED' && tagName != 'OBJECT') {
michael@0: // plugin overlay skipping until the target plugin is found
michael@0: isOverlay = true;
michael@0: containerElement = element;
michael@0: element = element.parentNode;
michael@0: if (!element) {
michael@0: throw new Error('Plugin element is not found');
michael@0: }
michael@0: tagName = element.nodeName;
michael@0: }
michael@0:
michael@0: if (isOverlay) {
michael@0: // Checking if overlay is a proper PlayPreview overlay.
michael@0: for (var i = 0; i < element.children.length; i++) {
michael@0: if (element.children[i] === containerElement) {
michael@0: throw new Error('Plugin element is invalid');
michael@0: }
michael@0: }
michael@0: }
michael@0: }
michael@0:
michael@0: if (element) {
michael@0: // Getting absolute URL from the EMBED tag
michael@0: url = element.srcURI.spec;
michael@0:
michael@0: pageUrl = element.ownerDocument.location.href; // proper page url?
michael@0:
michael@0: if (tagName == 'EMBED') {
michael@0: for (var i = 0; i < element.attributes.length; ++i) {
michael@0: var paramName = element.attributes[i].localName.toLowerCase();
michael@0: objectParams[paramName] = element.attributes[i].value;
michael@0: }
michael@0: } else {
michael@0: for (var i = 0; i < element.childNodes.length; ++i) {
michael@0: var paramElement = element.childNodes[i];
michael@0: if (paramElement.nodeType != 1 ||
michael@0: paramElement.nodeName != 'PARAM') {
michael@0: continue;
michael@0: }
michael@0: var paramName = paramElement.getAttribute('name').toLowerCase();
michael@0: objectParams[paramName] = paramElement.getAttribute('value');
michael@0: }
michael@0: }
michael@0: }
michael@0:
michael@0: if (!url) { // at this point url shall be known -- asserting
michael@0: throw new Error('Movie url is not specified');
michael@0: }
michael@0:
michael@0: baseUrl = objectParams.base || pageUrl;
michael@0:
michael@0: var movieParams = {};
michael@0: if (objectParams.flashvars) {
michael@0: movieParams = parseQueryString(objectParams.flashvars);
michael@0: }
michael@0: var queryStringMatch = /\?([^#]+)/.exec(url);
michael@0: if (queryStringMatch) {
michael@0: var queryStringParams = parseQueryString(queryStringMatch[1]);
michael@0: for (var i in queryStringParams) {
michael@0: if (!(i in movieParams)) {
michael@0: movieParams[i] = queryStringParams[i];
michael@0: }
michael@0: }
michael@0: }
michael@0:
michael@0: var allowScriptAccess = false;
michael@0: switch (objectParams.allowscriptaccess || 'sameDomain') {
michael@0: case 'always':
michael@0: allowScriptAccess = true;
michael@0: break;
michael@0: case 'never':
michael@0: allowScriptAccess = false;
michael@0: break;
michael@0: default:
michael@0: if (!pageUrl)
michael@0: break;
michael@0: try {
michael@0: // checking if page is in same domain (? same protocol and port)
michael@0: allowScriptAccess =
michael@0: Services.io.newURI('/', null, Services.io.newURI(pageUrl, null, null)).spec ==
michael@0: Services.io.newURI('/', null, Services.io.newURI(url, null, null)).spec;
michael@0: } catch (ex) {}
michael@0: break;
michael@0: }
michael@0:
michael@0: var actions = new ChromeActions(url, window, document);
michael@0: actions.objectParams = objectParams;
michael@0: actions.movieParams = movieParams;
michael@0: actions.baseUrl = baseUrl || url;
michael@0: actions.isOverlay = isOverlay;
michael@0: actions.embedTag = element;
michael@0: actions.isPausedAtStart = /\bpaused=true$/.test(urlHint);
michael@0: actions.allowScriptAccess = allowScriptAccess;
michael@0: return actions;
michael@0: },
michael@0:
michael@0: // nsIStreamConverter::asyncConvertData
michael@0: asyncConvertData: function(aFromType, aToType, aListener, aCtxt) {
michael@0: // Store the listener passed to us
michael@0: this.listener = aListener;
michael@0: },
michael@0:
michael@0: // nsIStreamListener::onDataAvailable
michael@0: onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) {
michael@0: // Do nothing since all the data loading is handled by the viewer.
michael@0: log('SANITY CHECK: onDataAvailable SHOULD NOT BE CALLED!');
michael@0: },
michael@0:
michael@0: // nsIRequestObserver::onStartRequest
michael@0: onStartRequest: function(aRequest, aContext) {
michael@0: // Setup the request so we can use it below.
michael@0: aRequest.QueryInterface(Ci.nsIChannel);
michael@0:
michael@0: aRequest.QueryInterface(Ci.nsIWritablePropertyBag);
michael@0:
michael@0: // Change the content type so we don't get stuck in a loop.
michael@0: aRequest.setProperty('contentType', aRequest.contentType);
michael@0: aRequest.contentType = 'text/html';
michael@0:
michael@0: // TODO For now suspending request, however we can continue fetching data
michael@0: aRequest.suspend();
michael@0:
michael@0: var originalURI = aRequest.URI;
michael@0:
michael@0: // checking if the plug-in shall be run in simple mode
michael@0: var isSimpleMode = originalURI.spec === EXPECTED_PLAYPREVIEW_URI_PREFIX &&
michael@0: getBoolPref('shumway.simpleMode', false);
michael@0:
michael@0: // Create a new channel that loads the viewer as a resource.
michael@0: var viewerUrl = isSimpleMode ?
michael@0: 'resource://shumway/web/simple.html' :
michael@0: 'resource://shumway/web/viewer.html';
michael@0: var channel = Services.io.newChannel(viewerUrl, null, null);
michael@0:
michael@0: var converter = this;
michael@0: var listener = this.listener;
michael@0: // Proxy all the request observer calls, when it gets to onStopRequest
michael@0: // we can get the dom window.
michael@0: var proxy = {
michael@0: onStartRequest: function(request, context) {
michael@0: listener.onStartRequest(aRequest, context);
michael@0: },
michael@0: onDataAvailable: function(request, context, inputStream, offset, count) {
michael@0: listener.onDataAvailable(aRequest, context, inputStream, offset, count);
michael@0: },
michael@0: onStopRequest: function(request, context, statusCode) {
michael@0: // Cancel the request so the viewer can handle it.
michael@0: aRequest.resume();
michael@0: aRequest.cancel(Cr.NS_BINDING_ABORTED);
michael@0:
michael@0: var domWindow = getDOMWindow(channel);
michael@0: let actions = converter.createChromeActions(domWindow,
michael@0: domWindow.document,
michael@0: converter.getUrlHint(originalURI));
michael@0:
michael@0: if (!isShumwayEnabledFor(actions)) {
michael@0: fallbackToNativePlugin(domWindow, false, true);
michael@0: return;
michael@0: }
michael@0:
michael@0: // Report telemetry on amount of swfs on the page
michael@0: if (actions.isOverlay) {
michael@0: // Looking for last actions with same baseUrl
michael@0: var prevPageActions = ActivationQueue.findLastOnPage(actions.baseUrl);
michael@0: var pageIndex = !prevPageActions ? 1 : (prevPageActions.telemetry.pageIndex + 1);
michael@0: actions.telemetry.pageIndex = pageIndex;
michael@0: ShumwayTelemetry.onPageIndex(pageIndex);
michael@0: } else {
michael@0: ShumwayTelemetry.onPageIndex(0);
michael@0: }
michael@0:
michael@0: actions.activationCallback = function(domWindow, isSimpleMode) {
michael@0: delete this.activationCallback;
michael@0: activateShumwayScripts(domWindow, isSimpleMode);
michael@0: }.bind(actions, domWindow, isSimpleMode);
michael@0: ActivationQueue.enqueue(actions);
michael@0:
michael@0: let requestListener = new RequestListener(actions);
michael@0: domWindow.addEventListener('shumway.message', function(event) {
michael@0: requestListener.receive(event);
michael@0: }, false, true);
michael@0:
michael@0: listener.onStopRequest(aRequest, context, statusCode);
michael@0: }
michael@0: };
michael@0:
michael@0: // Keep the URL the same so the browser sees it as the same.
michael@0: channel.originalURI = aRequest.URI;
michael@0: channel.loadGroup = aRequest.loadGroup;
michael@0:
michael@0: // We can use resource principal when data is fetched by the chrome
michael@0: // e.g. useful for NoScript
michael@0: var securityManager = Cc['@mozilla.org/scriptsecuritymanager;1']
michael@0: .getService(Ci.nsIScriptSecurityManager);
michael@0: var uri = Services.io.newURI(viewerUrl, null, null);
michael@0: var resourcePrincipal = securityManager.getNoAppCodebasePrincipal(uri);
michael@0: aRequest.owner = resourcePrincipal;
michael@0: channel.asyncOpen(proxy, aContext);
michael@0: },
michael@0:
michael@0: // nsIRequestObserver::onStopRequest
michael@0: onStopRequest: function(aRequest, aContext, aStatusCode) {
michael@0: // Do nothing.
michael@0: }
michael@0: };
michael@0:
michael@0: // properties required for XPCOM registration:
michael@0: function copyProperties(obj, template) {
michael@0: for (var prop in template) {
michael@0: obj[prop] = template[prop];
michael@0: }
michael@0: }
michael@0:
michael@0: function ShumwayStreamConverter() {}
michael@0: ShumwayStreamConverter.prototype = new ShumwayStreamConverterBase();
michael@0: copyProperties(ShumwayStreamConverter.prototype, {
michael@0: classID: Components.ID('{4c6030f7-e20a-264f-5b0e-ada3a9e97384}'),
michael@0: classDescription: 'Shumway Content Converter Component',
michael@0: contractID: '@mozilla.org/streamconv;1?from=application/x-shockwave-flash&to=*/*'
michael@0: });
michael@0:
michael@0: function ShumwayStreamOverlayConverter() {}
michael@0: ShumwayStreamOverlayConverter.prototype = new ShumwayStreamConverterBase();
michael@0: copyProperties(ShumwayStreamOverlayConverter.prototype, {
michael@0: classID: Components.ID('{4c6030f7-e20a-264f-5f9b-ada3a9e97384}'),
michael@0: classDescription: 'Shumway PlayPreview Component',
michael@0: contractID: '@mozilla.org/streamconv;1?from=application/x-moz-playpreview&to=*/*'
michael@0: });
michael@0: ShumwayStreamOverlayConverter.prototype.getUrlHint = function (requestUrl) {
michael@0: return '';
michael@0: };