1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/extensions/shumway/content/ShumwayStreamConverter.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1041 @@ 1.4 +/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */ 1.5 +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ 1.6 +/* 1.7 + * Copyright 2013 Mozilla Foundation 1.8 + * 1.9 + * Licensed under the Apache License, Version 2.0 (the "License"); 1.10 + * you may not use this file except in compliance with the License. 1.11 + * You may obtain a copy of the License at 1.12 + * 1.13 + * http://www.apache.org/licenses/LICENSE-2.0 1.14 + * 1.15 + * Unless required by applicable law or agreed to in writing, software 1.16 + * distributed under the License is distributed on an "AS IS" BASIS, 1.17 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1.18 + * See the License for the specific language governing permissions and 1.19 + * limitations under the License. 1.20 + */ 1.21 + 1.22 +'use strict'; 1.23 + 1.24 +var EXPORTED_SYMBOLS = ['ShumwayStreamConverter', 'ShumwayStreamOverlayConverter']; 1.25 + 1.26 +const Cc = Components.classes; 1.27 +const Ci = Components.interfaces; 1.28 +const Cr = Components.results; 1.29 +const Cu = Components.utils; 1.30 + 1.31 +const SHUMWAY_CONTENT_TYPE = 'application/x-shockwave-flash'; 1.32 +const EXPECTED_PLAYPREVIEW_URI_PREFIX = 'data:application/x-moz-playpreview;,' + 1.33 + SHUMWAY_CONTENT_TYPE; 1.34 + 1.35 +const FIREFOX_ID = '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}'; 1.36 +const SEAMONKEY_ID = '{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}'; 1.37 + 1.38 +const MAX_CLIPBOARD_DATA_SIZE = 8000; 1.39 + 1.40 +Cu.import('resource://gre/modules/XPCOMUtils.jsm'); 1.41 +Cu.import('resource://gre/modules/Services.jsm'); 1.42 +Cu.import('resource://gre/modules/NetUtil.jsm'); 1.43 +Cu.import('resource://gre/modules/Promise.jsm'); 1.44 + 1.45 +XPCOMUtils.defineLazyModuleGetter(this, 'PrivateBrowsingUtils', 1.46 + 'resource://gre/modules/PrivateBrowsingUtils.jsm'); 1.47 + 1.48 +XPCOMUtils.defineLazyModuleGetter(this, 'AddonManager', 1.49 + 'resource://gre/modules/AddonManager.jsm'); 1.50 + 1.51 +XPCOMUtils.defineLazyModuleGetter(this, 'ShumwayTelemetry', 1.52 + 'resource://shumway/ShumwayTelemetry.jsm'); 1.53 + 1.54 +let Svc = {}; 1.55 +XPCOMUtils.defineLazyServiceGetter(Svc, 'mime', 1.56 + '@mozilla.org/mime;1', 'nsIMIMEService'); 1.57 + 1.58 +let StringInputStream = Cc["@mozilla.org/io/string-input-stream;1"]; 1.59 +let MimeInputStream = Cc["@mozilla.org/network/mime-input-stream;1"]; 1.60 + 1.61 +function getBoolPref(pref, def) { 1.62 + try { 1.63 + return Services.prefs.getBoolPref(pref); 1.64 + } catch (ex) { 1.65 + return def; 1.66 + } 1.67 +} 1.68 + 1.69 +function getStringPref(pref, def) { 1.70 + try { 1.71 + return Services.prefs.getComplexValue(pref, Ci.nsISupportsString).data; 1.72 + } catch (ex) { 1.73 + return def; 1.74 + } 1.75 +} 1.76 + 1.77 +function log(aMsg) { 1.78 + let msg = 'ShumwayStreamConverter.js: ' + (aMsg.join ? aMsg.join('') : aMsg); 1.79 + Services.console.logStringMessage(msg); 1.80 + dump(msg + '\n'); 1.81 +} 1.82 + 1.83 +function getDOMWindow(aChannel) { 1.84 + var requestor = aChannel.notificationCallbacks || 1.85 + aChannel.loadGroup.notificationCallbacks; 1.86 + var win = requestor.getInterface(Components.interfaces.nsIDOMWindow); 1.87 + return win; 1.88 +} 1.89 + 1.90 +function parseQueryString(qs) { 1.91 + if (!qs) 1.92 + return {}; 1.93 + 1.94 + if (qs.charAt(0) == '?') 1.95 + qs = qs.slice(1); 1.96 + 1.97 + var values = qs.split('&'); 1.98 + var obj = {}; 1.99 + for (var i = 0; i < values.length; i++) { 1.100 + var kv = values[i].split('='); 1.101 + var key = kv[0], value = kv[1]; 1.102 + obj[decodeURIComponent(key)] = decodeURIComponent(value); 1.103 + } 1.104 + 1.105 + return obj; 1.106 +} 1.107 + 1.108 +function domainMatches(host, pattern) { 1.109 + if (!pattern) return false; 1.110 + if (pattern === '*') return true; 1.111 + host = host.toLowerCase(); 1.112 + var parts = pattern.toLowerCase().split('*'); 1.113 + if (host.indexOf(parts[0]) !== 0) return false; 1.114 + var p = parts[0].length; 1.115 + for (var i = 1; i < parts.length; i++) { 1.116 + var j = host.indexOf(parts[i], p); 1.117 + if (j === -1) return false; 1.118 + p = j + parts[i].length; 1.119 + } 1.120 + return parts[parts.length - 1] === '' || p === host.length; 1.121 +} 1.122 + 1.123 +function fetchPolicyFile(url, cache, callback) { 1.124 + if (url in cache) { 1.125 + return callback(cache[url]); 1.126 + } 1.127 + 1.128 + log('Fetching policy file at ' + url); 1.129 + var MAX_POLICY_SIZE = 8192; 1.130 + var xhr = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] 1.131 + .createInstance(Ci.nsIXMLHttpRequest); 1.132 + xhr.open('GET', url, true); 1.133 + xhr.overrideMimeType('text/xml'); 1.134 + xhr.onprogress = function (e) { 1.135 + if (e.loaded >= MAX_POLICY_SIZE) { 1.136 + xhr.abort(); 1.137 + cache[url] = false; 1.138 + callback(null, 'Max policy size'); 1.139 + } 1.140 + }; 1.141 + xhr.onreadystatechange = function(event) { 1.142 + if (xhr.readyState === 4) { 1.143 + // TODO disable redirects 1.144 + var doc = xhr.responseXML; 1.145 + if (xhr.status !== 200 || !doc) { 1.146 + cache[url] = false; 1.147 + return callback(null, 'Invalid HTTP status: ' + xhr.statusText); 1.148 + } 1.149 + // parsing params 1.150 + var params = doc.documentElement.childNodes; 1.151 + var policy = { siteControl: null, allowAccessFrom: []}; 1.152 + for (var i = 0; i < params.length; i++) { 1.153 + switch (params[i].localName) { 1.154 + case 'site-control': 1.155 + policy.siteControl = params[i].getAttribute('permitted-cross-domain-policies'); 1.156 + break; 1.157 + case 'allow-access-from': 1.158 + var access = { 1.159 + domain: params[i].getAttribute('domain'), 1.160 + security: params[i].getAttribute('security') === 'true' 1.161 + }; 1.162 + policy.allowAccessFrom.push(access); 1.163 + break; 1.164 + default: 1.165 + // TODO allow-http-request-headers-from and other 1.166 + break; 1.167 + } 1.168 + } 1.169 + callback(cache[url] = policy); 1.170 + } 1.171 + }; 1.172 + xhr.send(null); 1.173 +} 1.174 + 1.175 +function isShumwayEnabledFor(actions) { 1.176 + // disabled for PrivateBrowsing windows 1.177 + if (PrivateBrowsingUtils.isWindowPrivate(actions.window)) { 1.178 + return false; 1.179 + } 1.180 + // disabled if embed tag specifies shumwaymode (for testing purpose) 1.181 + if (actions.objectParams['shumwaymode'] === 'off') { 1.182 + return false; 1.183 + } 1.184 + 1.185 + var url = actions.url; 1.186 + var baseUrl = actions.baseUrl; 1.187 + 1.188 + // blacklisting well known sites with issues 1.189 + if (/\.ytimg\.com\//i.test(url) /* youtube movies */ || 1.190 + /\/vui.swf\b/i.test(url) /* vidyo manager */ || 1.191 + /soundcloud\.com\/player\/assets\/swf/i.test(url) /* soundcloud */ || 1.192 + /sndcdn\.com\/assets\/swf/.test(url) /* soundcloud */ || 1.193 + /vimeocdn\.com/.test(url) /* vimeo */) { 1.194 + return false; 1.195 + } 1.196 + 1.197 + return true; 1.198 +} 1.199 + 1.200 +function getVersionInfo() { 1.201 + var deferred = Promise.defer(); 1.202 + var versionInfo = { 1.203 + geckoMstone : 'unknown', 1.204 + geckoBuildID: 'unknown', 1.205 + shumwayVersion: 'unknown' 1.206 + }; 1.207 + try { 1.208 + versionInfo.geckoMstone = Services.prefs.getCharPref('gecko.mstone'); 1.209 + versionInfo.geckoBuildID = Services.prefs.getCharPref('gecko.buildID'); 1.210 + } catch (e) { 1.211 + log('Error encountered while getting platform version info:', e); 1.212 + } 1.213 + try { 1.214 + var addonId = "shumway@research.mozilla.org"; 1.215 + AddonManager.getAddonByID(addonId, function(addon) { 1.216 + versionInfo.shumwayVersion = addon ? addon.version : 'n/a'; 1.217 + deferred.resolve(versionInfo); 1.218 + }); 1.219 + } catch (e) { 1.220 + log('Error encountered while getting Shumway version info:', e); 1.221 + deferred.resolve(versionInfo); 1.222 + } 1.223 + return deferred.promise; 1.224 +} 1.225 + 1.226 +function fallbackToNativePlugin(window, userAction, activateCTP) { 1.227 + var obj = window.frameElement; 1.228 + var doc = obj.ownerDocument; 1.229 + var e = doc.createEvent("CustomEvent"); 1.230 + e.initCustomEvent("MozPlayPlugin", true, true, activateCTP); 1.231 + obj.dispatchEvent(e); 1.232 + 1.233 + ShumwayTelemetry.onFallback(userAction); 1.234 +} 1.235 + 1.236 +// All the priviledged actions. 1.237 +function ChromeActions(url, window, document) { 1.238 + this.url = url; 1.239 + this.objectParams = null; 1.240 + this.movieParams = null; 1.241 + this.baseUrl = url; 1.242 + this.isOverlay = false; 1.243 + this.isPausedAtStart = false; 1.244 + this.window = window; 1.245 + this.document = document; 1.246 + this.externalComInitialized = false; 1.247 + this.allowScriptAccess = false; 1.248 + this.crossdomainRequestsCache = Object.create(null); 1.249 + this.telemetry = { 1.250 + startTime: Date.now(), 1.251 + features: [], 1.252 + errors: [], 1.253 + pageIndex: 0 1.254 + }; 1.255 +} 1.256 + 1.257 +ChromeActions.prototype = { 1.258 + getBoolPref: function (data) { 1.259 + if (!/^shumway\./.test(data.pref)) { 1.260 + return null; 1.261 + } 1.262 + return getBoolPref(data.pref, data.def); 1.263 + }, 1.264 + getCompilerSettings: function getCompilerSettings() { 1.265 + return JSON.stringify({ 1.266 + appCompiler: getBoolPref('shumway.appCompiler', true), 1.267 + sysCompiler: getBoolPref('shumway.sysCompiler', false), 1.268 + verifier: getBoolPref('shumway.verifier', true) 1.269 + }); 1.270 + }, 1.271 + addProfilerMarker: function (marker) { 1.272 + if ('nsIProfiler' in Ci) { 1.273 + let profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler); 1.274 + profiler.AddMarker(marker); 1.275 + } 1.276 + }, 1.277 + getPluginParams: function getPluginParams() { 1.278 + return JSON.stringify({ 1.279 + url: this.url, 1.280 + baseUrl : this.baseUrl, 1.281 + movieParams: this.movieParams, 1.282 + objectParams: this.objectParams, 1.283 + isOverlay: this.isOverlay, 1.284 + isPausedAtStart: this.isPausedAtStart 1.285 + }); 1.286 + }, 1.287 + _canDownloadFile: function canDownloadFile(data, callback) { 1.288 + var url = data.url, checkPolicyFile = data.checkPolicyFile; 1.289 + 1.290 + // TODO flash cross-origin request 1.291 + if (url === this.url) { 1.292 + // allow downloading for the original file 1.293 + return callback({success: true}); 1.294 + } 1.295 + 1.296 + // allows downloading from the same origin 1.297 + var parsedUrl, parsedBaseUrl; 1.298 + try { 1.299 + parsedUrl = NetUtil.newURI(url); 1.300 + } catch (ex) { /* skipping invalid urls */ } 1.301 + try { 1.302 + parsedBaseUrl = NetUtil.newURI(this.url); 1.303 + } catch (ex) { /* skipping invalid urls */ } 1.304 + 1.305 + if (parsedUrl && parsedBaseUrl && 1.306 + parsedUrl.prePath === parsedBaseUrl.prePath) { 1.307 + return callback({success: true}); 1.308 + } 1.309 + 1.310 + // additionally using internal whitelist 1.311 + var whitelist = getStringPref('shumway.whitelist', ''); 1.312 + if (whitelist && parsedUrl) { 1.313 + var whitelisted = whitelist.split(',').some(function (i) { 1.314 + return domainMatches(parsedUrl.host, i); 1.315 + }); 1.316 + if (whitelisted) { 1.317 + return callback({success: true}); 1.318 + } 1.319 + } 1.320 + 1.321 + if (!checkPolicyFile || !parsedUrl || !parsedBaseUrl) { 1.322 + return callback({success: false}); 1.323 + } 1.324 + 1.325 + // we can request crossdomain.xml 1.326 + fetchPolicyFile(parsedUrl.prePath + '/crossdomain.xml', this.crossdomainRequestsCache, 1.327 + function (policy, error) { 1.328 + 1.329 + if (!policy || policy.siteControl === 'none') { 1.330 + return callback({success: false}); 1.331 + } 1.332 + // TODO assuming master-only, there are also 'by-content-type', 'all', etc. 1.333 + 1.334 + var allowed = policy.allowAccessFrom.some(function (i) { 1.335 + return domainMatches(parsedBaseUrl.host, i.domain) && 1.336 + (!i.secure || parsedBaseUrl.scheme.toLowerCase() === 'https'); 1.337 + }); 1.338 + return callback({success: allowed}); 1.339 + }.bind(this)); 1.340 + }, 1.341 + loadFile: function loadFile(data) { 1.342 + var url = data.url; 1.343 + var checkPolicyFile = data.checkPolicyFile; 1.344 + var sessionId = data.sessionId; 1.345 + var limit = data.limit || 0; 1.346 + var method = data.method || "GET"; 1.347 + var mimeType = data.mimeType; 1.348 + var postData = data.postData || null; 1.349 + 1.350 + var win = this.window; 1.351 + var baseUrl = this.baseUrl; 1.352 + 1.353 + var performXHR = function () { 1.354 + var xhr = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"] 1.355 + .createInstance(Ci.nsIXMLHttpRequest); 1.356 + xhr.open(method, url, true); 1.357 + xhr.responseType = "moz-chunked-arraybuffer"; 1.358 + 1.359 + if (baseUrl) { 1.360 + // Setting the referer uri, some site doing checks if swf is embedded 1.361 + // on the original page. 1.362 + xhr.setRequestHeader("Referer", baseUrl); 1.363 + } 1.364 + 1.365 + // TODO apply range request headers if limit is specified 1.366 + 1.367 + var lastPosition = 0; 1.368 + xhr.onprogress = function (e) { 1.369 + var position = e.loaded; 1.370 + var data = new Uint8Array(xhr.response); 1.371 + win.postMessage({callback:"loadFile", sessionId: sessionId, topic: "progress", 1.372 + array: data, loaded: e.loaded, total: e.total}, "*"); 1.373 + lastPosition = position; 1.374 + if (limit && e.total >= limit) { 1.375 + xhr.abort(); 1.376 + } 1.377 + }; 1.378 + xhr.onreadystatechange = function(event) { 1.379 + if (xhr.readyState === 4) { 1.380 + if (xhr.status !== 200 && xhr.status !== 0) { 1.381 + win.postMessage({callback:"loadFile", sessionId: sessionId, topic: "error", 1.382 + error: xhr.statusText}, "*"); 1.383 + } 1.384 + win.postMessage({callback:"loadFile", sessionId: sessionId, topic: "close"}, "*"); 1.385 + } 1.386 + }; 1.387 + if (mimeType) 1.388 + xhr.setRequestHeader("Content-Type", mimeType); 1.389 + xhr.send(postData); 1.390 + win.postMessage({callback:"loadFile", sessionId: sessionId, topic: "open"}, "*"); 1.391 + }; 1.392 + 1.393 + this._canDownloadFile({url: url, checkPolicyFile: checkPolicyFile}, function (data) { 1.394 + if (data.success) { 1.395 + performXHR(); 1.396 + } else { 1.397 + log("data access id prohibited to " + url + " from " + baseUrl); 1.398 + win.postMessage({callback:"loadFile", sessionId: sessionId, topic: "error", 1.399 + error: "only original swf file or file from the same origin loading supported"}, "*"); 1.400 + } 1.401 + }); 1.402 + }, 1.403 + fallback: function(automatic) { 1.404 + automatic = !!automatic; 1.405 + fallbackToNativePlugin(this.window, !automatic, automatic); 1.406 + }, 1.407 + setClipboard: function (data) { 1.408 + if (typeof data !== 'string' || 1.409 + data.length > MAX_CLIPBOARD_DATA_SIZE || 1.410 + !this.document.hasFocus()) { 1.411 + return; 1.412 + } 1.413 + // TODO other security checks? 1.414 + 1.415 + let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"] 1.416 + .getService(Ci.nsIClipboardHelper); 1.417 + clipboard.copyString(data); 1.418 + }, 1.419 + unsafeSetClipboard: function (data) { 1.420 + if (typeof data !== 'string') { 1.421 + return; 1.422 + } 1.423 + let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper); 1.424 + clipboard.copyString(data); 1.425 + }, 1.426 + endActivation: function () { 1.427 + if (ActivationQueue.currentNonActive === this) { 1.428 + ActivationQueue.activateNext(); 1.429 + } 1.430 + }, 1.431 + reportTelemetry: function (data) { 1.432 + var topic = data.topic; 1.433 + switch (topic) { 1.434 + case 'firstFrame': 1.435 + var time = Date.now() - this.telemetry.startTime; 1.436 + ShumwayTelemetry.onFirstFrame(time); 1.437 + break; 1.438 + case 'parseInfo': 1.439 + ShumwayTelemetry.onParseInfo({ 1.440 + parseTime: +data.parseTime, 1.441 + size: +data.bytesTotal, 1.442 + swfVersion: data.swfVersion|0, 1.443 + frameRate: +data.frameRate, 1.444 + width: data.width|0, 1.445 + height: data.height|0, 1.446 + bannerType: data.bannerType|0, 1.447 + isAvm2: !!data.isAvm2 1.448 + }); 1.449 + break; 1.450 + case 'feature': 1.451 + var featureType = data.feature|0; 1.452 + var MIN_FEATURE_TYPE = 0, MAX_FEATURE_TYPE = 999; 1.453 + if (featureType >= MIN_FEATURE_TYPE && featureType <= MAX_FEATURE_TYPE && 1.454 + !this.telemetry.features[featureType]) { 1.455 + this.telemetry.features[featureType] = true; // record only one feature per SWF 1.456 + ShumwayTelemetry.onFeature(featureType); 1.457 + } 1.458 + break; 1.459 + case 'error': 1.460 + var errorType = data.error|0; 1.461 + var MIN_ERROR_TYPE = 0, MAX_ERROR_TYPE = 2; 1.462 + if (errorType >= MIN_ERROR_TYPE && errorType <= MAX_ERROR_TYPE && 1.463 + !this.telemetry.errors[errorType]) { 1.464 + this.telemetry.errors[errorType] = true; // record only one report per SWF 1.465 + ShumwayTelemetry.onError(errorType); 1.466 + } 1.467 + break; 1.468 + } 1.469 + }, 1.470 + reportIssue: function(exceptions) { 1.471 + var base = "http://shumway-issue-reporter.paas.allizom.org/input?"; 1.472 + var windowUrl = this.window.parent.wrappedJSObject.location + ''; 1.473 + var params = 'url=' + encodeURIComponent(windowUrl); 1.474 + params += '&swf=' + encodeURIComponent(this.url); 1.475 + getVersionInfo().then(function (versions) { 1.476 + params += '&ffbuild=' + encodeURIComponent(versions.geckoMstone + ' (' + 1.477 + versions.geckoBuildID + ')'); 1.478 + params += '&shubuild=' + encodeURIComponent(versions.shumwayVersion); 1.479 + }).then(function () { 1.480 + var postDataStream = StringInputStream. 1.481 + createInstance(Ci.nsIStringInputStream); 1.482 + postDataStream.data = 'exceptions=' + encodeURIComponent(exceptions); 1.483 + var postData = MimeInputStream.createInstance(Ci.nsIMIMEInputStream); 1.484 + postData.addHeader("Content-Type", "application/x-www-form-urlencoded"); 1.485 + postData.addContentLength = true; 1.486 + postData.setData(postDataStream); 1.487 + this.window.openDialog('chrome://browser/content', '_blank', 1.488 + 'all,dialog=no', base + params, null, null, 1.489 + postData); 1.490 + }.bind(this)); 1.491 + }, 1.492 + externalCom: function (data) { 1.493 + if (!this.allowScriptAccess) 1.494 + return; 1.495 + 1.496 + // TODO check security ? 1.497 + var parentWindow = this.window.parent.wrappedJSObject; 1.498 + var embedTag = this.embedTag.wrappedJSObject; 1.499 + switch (data.action) { 1.500 + case 'init': 1.501 + if (this.externalComInitialized) 1.502 + return; 1.503 + 1.504 + this.externalComInitialized = true; 1.505 + var eventTarget = this.window.document; 1.506 + initExternalCom(parentWindow, embedTag, eventTarget); 1.507 + return; 1.508 + case 'getId': 1.509 + return embedTag.id; 1.510 + case 'eval': 1.511 + return parentWindow.__flash__eval(data.expression); 1.512 + case 'call': 1.513 + return parentWindow.__flash__call(data.request); 1.514 + case 'register': 1.515 + return embedTag.__flash__registerCallback(data.functionName); 1.516 + case 'unregister': 1.517 + return embedTag.__flash__unregisterCallback(data.functionName); 1.518 + } 1.519 + }, 1.520 + getWindowUrl: function() { 1.521 + return this.window.parent.wrappedJSObject.location + ''; 1.522 + } 1.523 +}; 1.524 + 1.525 +// Event listener to trigger chrome privedged code. 1.526 +function RequestListener(actions) { 1.527 + this.actions = actions; 1.528 +} 1.529 +// Receive an event and synchronously or asynchronously responds. 1.530 +RequestListener.prototype.receive = function(event) { 1.531 + var message = event.target; 1.532 + var action = event.detail.action; 1.533 + var data = event.detail.data; 1.534 + var sync = event.detail.sync; 1.535 + var actions = this.actions; 1.536 + if (!(action in actions)) { 1.537 + log('Unknown action: ' + action); 1.538 + return; 1.539 + } 1.540 + if (sync) { 1.541 + var response = actions[action].call(this.actions, data); 1.542 + var detail = event.detail; 1.543 + detail.__exposedProps__ = {response: 'r'}; 1.544 + detail.response = response; 1.545 + } else { 1.546 + var response; 1.547 + if (event.detail.callback) { 1.548 + var cookie = event.detail.cookie; 1.549 + response = function sendResponse(response) { 1.550 + var doc = actions.document; 1.551 + try { 1.552 + var listener = doc.createEvent('CustomEvent'); 1.553 + listener.initCustomEvent('shumway.response', true, false, 1.554 + {response: response, 1.555 + cookie: cookie, 1.556 + __exposedProps__: {response: 'r', cookie: 'r'}}); 1.557 + 1.558 + return message.dispatchEvent(listener); 1.559 + } catch (e) { 1.560 + // doc is no longer accessible because the requestor is already 1.561 + // gone. unloaded content cannot receive the response anyway. 1.562 + } 1.563 + }; 1.564 + } 1.565 + actions[action].call(this.actions, data, response); 1.566 + } 1.567 +}; 1.568 + 1.569 +var ActivationQueue = { 1.570 + nonActive: [], 1.571 + initializing: -1, 1.572 + activationTimeout: null, 1.573 + get currentNonActive() { 1.574 + return this.nonActive[this.initializing]; 1.575 + }, 1.576 + enqueue: function ActivationQueue_enqueue(actions) { 1.577 + this.nonActive.push(actions); 1.578 + if (this.nonActive.length === 1) { 1.579 + this.activateNext(); 1.580 + } 1.581 + }, 1.582 + findLastOnPage: function ActivationQueue_findLastOnPage(baseUrl) { 1.583 + for (var i = this.nonActive.length - 1; i >= 0; i--) { 1.584 + if (this.nonActive[i].baseUrl === baseUrl) { 1.585 + return this.nonActive[i]; 1.586 + } 1.587 + } 1.588 + return null; 1.589 + }, 1.590 + activateNext: function ActivationQueue_activateNext() { 1.591 + function weightInstance(actions) { 1.592 + // set of heuristics for find the most important instance to load 1.593 + var weight = 0; 1.594 + // using linear distance to the top-left of the view area 1.595 + if (actions.embedTag) { 1.596 + var window = actions.window; 1.597 + var clientRect = actions.embedTag.getBoundingClientRect(); 1.598 + weight -= Math.abs(clientRect.left - window.scrollX) + 1.599 + Math.abs(clientRect.top - window.scrollY); 1.600 + } 1.601 + var doc = actions.document; 1.602 + if (!doc.hidden) { 1.603 + weight += 100000; // might not be that important if hidden 1.604 + } 1.605 + if (actions.embedTag && 1.606 + actions.embedTag.ownerDocument.hasFocus()) { 1.607 + weight += 10000; // parent document is focused 1.608 + } 1.609 + return weight; 1.610 + } 1.611 + 1.612 + if (this.activationTimeout) { 1.613 + this.activationTimeout.cancel(); 1.614 + this.activationTimeout = null; 1.615 + } 1.616 + 1.617 + if (this.initializing >= 0) { 1.618 + this.nonActive.splice(this.initializing, 1); 1.619 + } 1.620 + var weights = []; 1.621 + for (var i = 0; i < this.nonActive.length; i++) { 1.622 + try { 1.623 + var weight = weightInstance(this.nonActive[i]); 1.624 + weights.push(weight); 1.625 + } catch (ex) { 1.626 + // unable to calc weight the instance, removing 1.627 + log('Shumway instance weight calculation failed: ' + ex); 1.628 + this.nonActive.splice(i, 1); 1.629 + i--; 1.630 + } 1.631 + } 1.632 + 1.633 + do { 1.634 + if (this.nonActive.length === 0) { 1.635 + this.initializing = -1; 1.636 + return; 1.637 + } 1.638 + 1.639 + var maxWeightIndex = 0; 1.640 + var maxWeight = weights[0]; 1.641 + for (var i = 1; i < weights.length; i++) { 1.642 + if (maxWeight < weights[i]) { 1.643 + maxWeight = weights[i]; 1.644 + maxWeightIndex = i; 1.645 + } 1.646 + } 1.647 + try { 1.648 + this.initializing = maxWeightIndex; 1.649 + this.nonActive[maxWeightIndex].activationCallback(); 1.650 + break; 1.651 + } catch (ex) { 1.652 + // unable to initialize the instance, trying another one 1.653 + log('Shumway instance initialization failed: ' + ex); 1.654 + this.nonActive.splice(maxWeightIndex, 1); 1.655 + weights.splice(maxWeightIndex, 1); 1.656 + } 1.657 + } while (true); 1.658 + 1.659 + var ACTIVATION_TIMEOUT = 3000; 1.660 + this.activationTimeout = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 1.661 + this.activationTimeout.initWithCallback(function () { 1.662 + log('Timeout during shumway instance initialization'); 1.663 + this.activateNext(); 1.664 + }.bind(this), ACTIVATION_TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT); 1.665 + } 1.666 +}; 1.667 + 1.668 +function activateShumwayScripts(window, preview) { 1.669 + function loadScripts(scripts, callback) { 1.670 + function scriptLoaded() { 1.671 + leftToLoad--; 1.672 + if (leftToLoad === 0) { 1.673 + callback(); 1.674 + } 1.675 + } 1.676 + var leftToLoad = scripts.length; 1.677 + var document = window.document.wrappedJSObject; 1.678 + var head = document.getElementsByTagName('head')[0]; 1.679 + for (var i = 0; i < scripts.length; i++) { 1.680 + var script = document.createElement('script'); 1.681 + script.type = "text/javascript"; 1.682 + script.src = scripts[i]; 1.683 + script.onload = scriptLoaded; 1.684 + head.appendChild(script); 1.685 + } 1.686 + } 1.687 + 1.688 + function initScripts() { 1.689 + if (preview) { 1.690 + loadScripts(['resource://shumway/web/preview.js'], function () { 1.691 + window.wrappedJSObject.runSniffer(); 1.692 + }); 1.693 + } else { 1.694 + loadScripts(['resource://shumway/shumway.js', 1.695 + 'resource://shumway/web/avm-sandbox.js'], function () { 1.696 + window.wrappedJSObject.runViewer(); 1.697 + }); 1.698 + } 1.699 + } 1.700 + 1.701 + window.wrappedJSObject.SHUMWAY_ROOT = "resource://shumway/"; 1.702 + 1.703 + if (window.document.readyState === "interactive" || 1.704 + window.document.readyState === "complete") { 1.705 + initScripts(); 1.706 + } else { 1.707 + window.document.addEventListener('DOMContentLoaded', initScripts); 1.708 + } 1.709 +} 1.710 + 1.711 +function initExternalCom(wrappedWindow, wrappedObject, targetDocument) { 1.712 + if (!wrappedWindow.__flash__initialized) { 1.713 + wrappedWindow.__flash__initialized = true; 1.714 + wrappedWindow.__flash__toXML = function __flash__toXML(obj) { 1.715 + switch (typeof obj) { 1.716 + case 'boolean': 1.717 + return obj ? '<true/>' : '<false/>'; 1.718 + case 'number': 1.719 + return '<number>' + obj + '</number>'; 1.720 + case 'object': 1.721 + if (obj === null) { 1.722 + return '<null/>'; 1.723 + } 1.724 + if ('hasOwnProperty' in obj && obj.hasOwnProperty('length')) { 1.725 + // array 1.726 + var xml = '<array>'; 1.727 + for (var i = 0; i < obj.length; i++) { 1.728 + xml += '<property id="' + i + '">' + __flash__toXML(obj[i]) + '</property>'; 1.729 + } 1.730 + return xml + '</array>'; 1.731 + } 1.732 + var xml = '<object>'; 1.733 + for (var i in obj) { 1.734 + xml += '<property id="' + i + '">' + __flash__toXML(obj[i]) + '</property>'; 1.735 + } 1.736 + return xml + '</object>'; 1.737 + case 'string': 1.738 + return '<string>' + obj.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') + '</string>'; 1.739 + case 'undefined': 1.740 + return '<undefined/>'; 1.741 + } 1.742 + }; 1.743 + var sandbox = new Cu.Sandbox(wrappedWindow, {sandboxPrototype: wrappedWindow}); 1.744 + wrappedWindow.__flash__eval = function (evalInSandbox, sandbox, expr) { 1.745 + this.console.log('__flash__eval: ' + expr); 1.746 + return evalInSandbox(expr, sandbox); 1.747 + }.bind(wrappedWindow, Cu.evalInSandbox, sandbox); 1.748 + wrappedWindow.__flash__call = function (expr) { 1.749 + this.console.log('__flash__call (ignored): ' + expr); 1.750 + }; 1.751 + } 1.752 + wrappedObject.__flash__registerCallback = function (functionName) { 1.753 + wrappedWindow.console.log('__flash__registerCallback: ' + functionName); 1.754 + this[functionName] = function () { 1.755 + var args = Array.prototype.slice.call(arguments, 0); 1.756 + wrappedWindow.console.log('__flash__callIn: ' + functionName); 1.757 + var e = targetDocument.createEvent('CustomEvent'); 1.758 + e.initCustomEvent('shumway.remote', true, false, { 1.759 + functionName: functionName, 1.760 + args: args, 1.761 + __exposedProps__: {args: 'r', functionName: 'r', result: 'rw'} 1.762 + }); 1.763 + targetDocument.dispatchEvent(e); 1.764 + return e.detail.result; 1.765 + }; 1.766 + }; 1.767 + wrappedObject.__flash__unregisterCallback = function (functionName) { 1.768 + wrappedWindow.console.log('__flash__unregisterCallback: ' + functionName); 1.769 + delete this[functionName]; 1.770 + }; 1.771 +} 1.772 + 1.773 +function ShumwayStreamConverterBase() { 1.774 +} 1.775 + 1.776 +ShumwayStreamConverterBase.prototype = { 1.777 + QueryInterface: XPCOMUtils.generateQI([ 1.778 + Ci.nsISupports, 1.779 + Ci.nsIStreamConverter, 1.780 + Ci.nsIStreamListener, 1.781 + Ci.nsIRequestObserver 1.782 + ]), 1.783 + 1.784 + /* 1.785 + * This component works as such: 1.786 + * 1. asyncConvertData stores the listener 1.787 + * 2. onStartRequest creates a new channel, streams the viewer and cancels 1.788 + * the request so Shumway can do the request 1.789 + * Since the request is cancelled onDataAvailable should not be called. The 1.790 + * onStopRequest does nothing. The convert function just returns the stream, 1.791 + * it's just the synchronous version of asyncConvertData. 1.792 + */ 1.793 + 1.794 + // nsIStreamConverter::convert 1.795 + convert: function(aFromStream, aFromType, aToType, aCtxt) { 1.796 + throw Cr.NS_ERROR_NOT_IMPLEMENTED; 1.797 + }, 1.798 + 1.799 + getUrlHint: function(requestUrl) { 1.800 + return requestUrl.spec; 1.801 + }, 1.802 + 1.803 + createChromeActions: function(window, document, urlHint) { 1.804 + var url = urlHint; 1.805 + var baseUrl; 1.806 + var pageUrl; 1.807 + var element = window.frameElement; 1.808 + var isOverlay = false; 1.809 + var objectParams = {}; 1.810 + if (element) { 1.811 + // PlayPreview overlay "belongs" to the embed/object tag and consists of 1.812 + // DIV and IFRAME. Starting from IFRAME and looking for first object tag. 1.813 + var tagName = element.nodeName, containerElement; 1.814 + while (tagName != 'EMBED' && tagName != 'OBJECT') { 1.815 + // plugin overlay skipping until the target plugin is found 1.816 + isOverlay = true; 1.817 + containerElement = element; 1.818 + element = element.parentNode; 1.819 + if (!element) { 1.820 + throw new Error('Plugin element is not found'); 1.821 + } 1.822 + tagName = element.nodeName; 1.823 + } 1.824 + 1.825 + if (isOverlay) { 1.826 + // Checking if overlay is a proper PlayPreview overlay. 1.827 + for (var i = 0; i < element.children.length; i++) { 1.828 + if (element.children[i] === containerElement) { 1.829 + throw new Error('Plugin element is invalid'); 1.830 + } 1.831 + } 1.832 + } 1.833 + } 1.834 + 1.835 + if (element) { 1.836 + // Getting absolute URL from the EMBED tag 1.837 + url = element.srcURI.spec; 1.838 + 1.839 + pageUrl = element.ownerDocument.location.href; // proper page url? 1.840 + 1.841 + if (tagName == 'EMBED') { 1.842 + for (var i = 0; i < element.attributes.length; ++i) { 1.843 + var paramName = element.attributes[i].localName.toLowerCase(); 1.844 + objectParams[paramName] = element.attributes[i].value; 1.845 + } 1.846 + } else { 1.847 + for (var i = 0; i < element.childNodes.length; ++i) { 1.848 + var paramElement = element.childNodes[i]; 1.849 + if (paramElement.nodeType != 1 || 1.850 + paramElement.nodeName != 'PARAM') { 1.851 + continue; 1.852 + } 1.853 + var paramName = paramElement.getAttribute('name').toLowerCase(); 1.854 + objectParams[paramName] = paramElement.getAttribute('value'); 1.855 + } 1.856 + } 1.857 + } 1.858 + 1.859 + if (!url) { // at this point url shall be known -- asserting 1.860 + throw new Error('Movie url is not specified'); 1.861 + } 1.862 + 1.863 + baseUrl = objectParams.base || pageUrl; 1.864 + 1.865 + var movieParams = {}; 1.866 + if (objectParams.flashvars) { 1.867 + movieParams = parseQueryString(objectParams.flashvars); 1.868 + } 1.869 + var queryStringMatch = /\?([^#]+)/.exec(url); 1.870 + if (queryStringMatch) { 1.871 + var queryStringParams = parseQueryString(queryStringMatch[1]); 1.872 + for (var i in queryStringParams) { 1.873 + if (!(i in movieParams)) { 1.874 + movieParams[i] = queryStringParams[i]; 1.875 + } 1.876 + } 1.877 + } 1.878 + 1.879 + var allowScriptAccess = false; 1.880 + switch (objectParams.allowscriptaccess || 'sameDomain') { 1.881 + case 'always': 1.882 + allowScriptAccess = true; 1.883 + break; 1.884 + case 'never': 1.885 + allowScriptAccess = false; 1.886 + break; 1.887 + default: 1.888 + if (!pageUrl) 1.889 + break; 1.890 + try { 1.891 + // checking if page is in same domain (? same protocol and port) 1.892 + allowScriptAccess = 1.893 + Services.io.newURI('/', null, Services.io.newURI(pageUrl, null, null)).spec == 1.894 + Services.io.newURI('/', null, Services.io.newURI(url, null, null)).spec; 1.895 + } catch (ex) {} 1.896 + break; 1.897 + } 1.898 + 1.899 + var actions = new ChromeActions(url, window, document); 1.900 + actions.objectParams = objectParams; 1.901 + actions.movieParams = movieParams; 1.902 + actions.baseUrl = baseUrl || url; 1.903 + actions.isOverlay = isOverlay; 1.904 + actions.embedTag = element; 1.905 + actions.isPausedAtStart = /\bpaused=true$/.test(urlHint); 1.906 + actions.allowScriptAccess = allowScriptAccess; 1.907 + return actions; 1.908 + }, 1.909 + 1.910 + // nsIStreamConverter::asyncConvertData 1.911 + asyncConvertData: function(aFromType, aToType, aListener, aCtxt) { 1.912 + // Store the listener passed to us 1.913 + this.listener = aListener; 1.914 + }, 1.915 + 1.916 + // nsIStreamListener::onDataAvailable 1.917 + onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) { 1.918 + // Do nothing since all the data loading is handled by the viewer. 1.919 + log('SANITY CHECK: onDataAvailable SHOULD NOT BE CALLED!'); 1.920 + }, 1.921 + 1.922 + // nsIRequestObserver::onStartRequest 1.923 + onStartRequest: function(aRequest, aContext) { 1.924 + // Setup the request so we can use it below. 1.925 + aRequest.QueryInterface(Ci.nsIChannel); 1.926 + 1.927 + aRequest.QueryInterface(Ci.nsIWritablePropertyBag); 1.928 + 1.929 + // Change the content type so we don't get stuck in a loop. 1.930 + aRequest.setProperty('contentType', aRequest.contentType); 1.931 + aRequest.contentType = 'text/html'; 1.932 + 1.933 + // TODO For now suspending request, however we can continue fetching data 1.934 + aRequest.suspend(); 1.935 + 1.936 + var originalURI = aRequest.URI; 1.937 + 1.938 + // checking if the plug-in shall be run in simple mode 1.939 + var isSimpleMode = originalURI.spec === EXPECTED_PLAYPREVIEW_URI_PREFIX && 1.940 + getBoolPref('shumway.simpleMode', false); 1.941 + 1.942 + // Create a new channel that loads the viewer as a resource. 1.943 + var viewerUrl = isSimpleMode ? 1.944 + 'resource://shumway/web/simple.html' : 1.945 + 'resource://shumway/web/viewer.html'; 1.946 + var channel = Services.io.newChannel(viewerUrl, null, null); 1.947 + 1.948 + var converter = this; 1.949 + var listener = this.listener; 1.950 + // Proxy all the request observer calls, when it gets to onStopRequest 1.951 + // we can get the dom window. 1.952 + var proxy = { 1.953 + onStartRequest: function(request, context) { 1.954 + listener.onStartRequest(aRequest, context); 1.955 + }, 1.956 + onDataAvailable: function(request, context, inputStream, offset, count) { 1.957 + listener.onDataAvailable(aRequest, context, inputStream, offset, count); 1.958 + }, 1.959 + onStopRequest: function(request, context, statusCode) { 1.960 + // Cancel the request so the viewer can handle it. 1.961 + aRequest.resume(); 1.962 + aRequest.cancel(Cr.NS_BINDING_ABORTED); 1.963 + 1.964 + var domWindow = getDOMWindow(channel); 1.965 + let actions = converter.createChromeActions(domWindow, 1.966 + domWindow.document, 1.967 + converter.getUrlHint(originalURI)); 1.968 + 1.969 + if (!isShumwayEnabledFor(actions)) { 1.970 + fallbackToNativePlugin(domWindow, false, true); 1.971 + return; 1.972 + } 1.973 + 1.974 + // Report telemetry on amount of swfs on the page 1.975 + if (actions.isOverlay) { 1.976 + // Looking for last actions with same baseUrl 1.977 + var prevPageActions = ActivationQueue.findLastOnPage(actions.baseUrl); 1.978 + var pageIndex = !prevPageActions ? 1 : (prevPageActions.telemetry.pageIndex + 1); 1.979 + actions.telemetry.pageIndex = pageIndex; 1.980 + ShumwayTelemetry.onPageIndex(pageIndex); 1.981 + } else { 1.982 + ShumwayTelemetry.onPageIndex(0); 1.983 + } 1.984 + 1.985 + actions.activationCallback = function(domWindow, isSimpleMode) { 1.986 + delete this.activationCallback; 1.987 + activateShumwayScripts(domWindow, isSimpleMode); 1.988 + }.bind(actions, domWindow, isSimpleMode); 1.989 + ActivationQueue.enqueue(actions); 1.990 + 1.991 + let requestListener = new RequestListener(actions); 1.992 + domWindow.addEventListener('shumway.message', function(event) { 1.993 + requestListener.receive(event); 1.994 + }, false, true); 1.995 + 1.996 + listener.onStopRequest(aRequest, context, statusCode); 1.997 + } 1.998 + }; 1.999 + 1.1000 + // Keep the URL the same so the browser sees it as the same. 1.1001 + channel.originalURI = aRequest.URI; 1.1002 + channel.loadGroup = aRequest.loadGroup; 1.1003 + 1.1004 + // We can use resource principal when data is fetched by the chrome 1.1005 + // e.g. useful for NoScript 1.1006 + var securityManager = Cc['@mozilla.org/scriptsecuritymanager;1'] 1.1007 + .getService(Ci.nsIScriptSecurityManager); 1.1008 + var uri = Services.io.newURI(viewerUrl, null, null); 1.1009 + var resourcePrincipal = securityManager.getNoAppCodebasePrincipal(uri); 1.1010 + aRequest.owner = resourcePrincipal; 1.1011 + channel.asyncOpen(proxy, aContext); 1.1012 + }, 1.1013 + 1.1014 + // nsIRequestObserver::onStopRequest 1.1015 + onStopRequest: function(aRequest, aContext, aStatusCode) { 1.1016 + // Do nothing. 1.1017 + } 1.1018 +}; 1.1019 + 1.1020 +// properties required for XPCOM registration: 1.1021 +function copyProperties(obj, template) { 1.1022 + for (var prop in template) { 1.1023 + obj[prop] = template[prop]; 1.1024 + } 1.1025 +} 1.1026 + 1.1027 +function ShumwayStreamConverter() {} 1.1028 +ShumwayStreamConverter.prototype = new ShumwayStreamConverterBase(); 1.1029 +copyProperties(ShumwayStreamConverter.prototype, { 1.1030 + classID: Components.ID('{4c6030f7-e20a-264f-5b0e-ada3a9e97384}'), 1.1031 + classDescription: 'Shumway Content Converter Component', 1.1032 + contractID: '@mozilla.org/streamconv;1?from=application/x-shockwave-flash&to=*/*' 1.1033 +}); 1.1034 + 1.1035 +function ShumwayStreamOverlayConverter() {} 1.1036 +ShumwayStreamOverlayConverter.prototype = new ShumwayStreamConverterBase(); 1.1037 +copyProperties(ShumwayStreamOverlayConverter.prototype, { 1.1038 + classID: Components.ID('{4c6030f7-e20a-264f-5f9b-ada3a9e97384}'), 1.1039 + classDescription: 'Shumway PlayPreview Component', 1.1040 + contractID: '@mozilla.org/streamconv;1?from=application/x-moz-playpreview&to=*/*' 1.1041 +}); 1.1042 +ShumwayStreamOverlayConverter.prototype.getUrlHint = function (requestUrl) { 1.1043 + return ''; 1.1044 +};