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: for (var i in obj) { michael@0: xml += '' + __flash__toXML(obj[i]) + ''; michael@0: } michael@0: return 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: };