browser/extensions/shumway/content/ShumwayStreamConverter.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

     1 /* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil; tab-width: 2 -*- */
     2 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
     3 /*
     4  * Copyright 2013 Mozilla Foundation
     5  *
     6  * Licensed under the Apache License, Version 2.0 (the "License");
     7  * you may not use this file except in compliance with the License.
     8  * You may obtain a copy of the License at
     9  *
    10  *     http://www.apache.org/licenses/LICENSE-2.0
    11  *
    12  * Unless required by applicable law or agreed to in writing, software
    13  * distributed under the License is distributed on an "AS IS" BASIS,
    14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15  * See the License for the specific language governing permissions and
    16  * limitations under the License.
    17  */
    19 'use strict';
    21 var EXPORTED_SYMBOLS = ['ShumwayStreamConverter', 'ShumwayStreamOverlayConverter'];
    23 const Cc = Components.classes;
    24 const Ci = Components.interfaces;
    25 const Cr = Components.results;
    26 const Cu = Components.utils;
    28 const SHUMWAY_CONTENT_TYPE = 'application/x-shockwave-flash';
    29 const EXPECTED_PLAYPREVIEW_URI_PREFIX = 'data:application/x-moz-playpreview;,' +
    30                                         SHUMWAY_CONTENT_TYPE;
    32 const FIREFOX_ID = '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}';
    33 const SEAMONKEY_ID = '{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}';
    35 const MAX_CLIPBOARD_DATA_SIZE = 8000;
    37 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
    38 Cu.import('resource://gre/modules/Services.jsm');
    39 Cu.import('resource://gre/modules/NetUtil.jsm');
    40 Cu.import('resource://gre/modules/Promise.jsm');
    42 XPCOMUtils.defineLazyModuleGetter(this, 'PrivateBrowsingUtils',
    43   'resource://gre/modules/PrivateBrowsingUtils.jsm');
    45 XPCOMUtils.defineLazyModuleGetter(this, 'AddonManager',
    46   'resource://gre/modules/AddonManager.jsm');
    48 XPCOMUtils.defineLazyModuleGetter(this, 'ShumwayTelemetry',
    49   'resource://shumway/ShumwayTelemetry.jsm');
    51 let Svc = {};
    52 XPCOMUtils.defineLazyServiceGetter(Svc, 'mime',
    53                                    '@mozilla.org/mime;1', 'nsIMIMEService');
    55 let StringInputStream = Cc["@mozilla.org/io/string-input-stream;1"];
    56 let MimeInputStream = Cc["@mozilla.org/network/mime-input-stream;1"];
    58 function getBoolPref(pref, def) {
    59   try {
    60     return Services.prefs.getBoolPref(pref);
    61   } catch (ex) {
    62     return def;
    63   }
    64 }
    66 function getStringPref(pref, def) {
    67   try {
    68     return Services.prefs.getComplexValue(pref, Ci.nsISupportsString).data;
    69   } catch (ex) {
    70     return def;
    71   }
    72 }
    74 function log(aMsg) {
    75   let msg = 'ShumwayStreamConverter.js: ' + (aMsg.join ? aMsg.join('') : aMsg);
    76   Services.console.logStringMessage(msg);
    77   dump(msg + '\n');
    78 }
    80 function getDOMWindow(aChannel) {
    81   var requestor = aChannel.notificationCallbacks ||
    82                   aChannel.loadGroup.notificationCallbacks;
    83   var win = requestor.getInterface(Components.interfaces.nsIDOMWindow);
    84   return win;
    85 }
    87 function parseQueryString(qs) {
    88   if (!qs)
    89     return {};
    91   if (qs.charAt(0) == '?')
    92     qs = qs.slice(1);
    94   var values = qs.split('&');
    95   var obj = {};
    96   for (var i = 0; i < values.length; i++) {
    97     var kv = values[i].split('=');
    98     var key = kv[0], value = kv[1];
    99     obj[decodeURIComponent(key)] = decodeURIComponent(value);
   100   }
   102   return obj;
   103 }
   105 function domainMatches(host, pattern) {
   106   if (!pattern) return false;
   107   if (pattern === '*') return true;
   108   host = host.toLowerCase();
   109   var parts = pattern.toLowerCase().split('*');
   110   if (host.indexOf(parts[0]) !== 0) return false;
   111   var p = parts[0].length;
   112   for (var i = 1; i < parts.length; i++) {
   113     var j = host.indexOf(parts[i], p);
   114     if (j === -1) return false;
   115     p = j + parts[i].length;
   116   }
   117   return parts[parts.length - 1] === '' || p === host.length;
   118 }
   120 function fetchPolicyFile(url, cache, callback) {
   121   if (url in cache) {
   122     return callback(cache[url]);
   123   }
   125   log('Fetching policy file at ' + url);
   126   var MAX_POLICY_SIZE = 8192;
   127   var xhr =  Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
   128                                .createInstance(Ci.nsIXMLHttpRequest);
   129   xhr.open('GET', url, true);
   130   xhr.overrideMimeType('text/xml');
   131   xhr.onprogress = function (e) {
   132     if (e.loaded >= MAX_POLICY_SIZE) {
   133       xhr.abort();
   134       cache[url] = false;
   135       callback(null, 'Max policy size');
   136     }
   137   };
   138   xhr.onreadystatechange = function(event) {
   139     if (xhr.readyState === 4) {
   140       // TODO disable redirects
   141       var doc = xhr.responseXML;
   142       if (xhr.status !== 200 || !doc) {
   143         cache[url] = false;
   144         return callback(null, 'Invalid HTTP status: ' + xhr.statusText);
   145       }
   146       // parsing params
   147       var params = doc.documentElement.childNodes;
   148       var policy = { siteControl: null, allowAccessFrom: []};
   149       for (var i = 0; i < params.length; i++) {
   150         switch (params[i].localName) {
   151         case 'site-control':
   152           policy.siteControl = params[i].getAttribute('permitted-cross-domain-policies');
   153           break;
   154         case 'allow-access-from':
   155           var access = {
   156             domain: params[i].getAttribute('domain'),
   157             security: params[i].getAttribute('security') === 'true'
   158           };
   159           policy.allowAccessFrom.push(access);
   160           break;
   161         default:
   162           // TODO allow-http-request-headers-from and other
   163           break;
   164         }
   165       }
   166       callback(cache[url] = policy);
   167     }
   168   };
   169   xhr.send(null);
   170 }
   172 function isShumwayEnabledFor(actions) {
   173   // disabled for PrivateBrowsing windows
   174   if (PrivateBrowsingUtils.isWindowPrivate(actions.window)) {
   175     return false;
   176   }
   177   // disabled if embed tag specifies shumwaymode (for testing purpose)
   178   if (actions.objectParams['shumwaymode'] === 'off') {
   179     return false;
   180   }
   182   var url = actions.url;
   183   var baseUrl = actions.baseUrl;
   185   // blacklisting well known sites with issues
   186   if (/\.ytimg\.com\//i.test(url) /* youtube movies */ ||
   187     /\/vui.swf\b/i.test(url) /* vidyo manager */  ||
   188     /soundcloud\.com\/player\/assets\/swf/i.test(url) /* soundcloud */ ||
   189     /sndcdn\.com\/assets\/swf/.test(url) /* soundcloud */ ||
   190     /vimeocdn\.com/.test(url) /* vimeo */) {
   191     return false;
   192   }
   194   return true;
   195 }
   197 function getVersionInfo() {
   198   var deferred = Promise.defer();
   199   var versionInfo = {
   200     geckoMstone : 'unknown',
   201     geckoBuildID: 'unknown',
   202     shumwayVersion: 'unknown'
   203   };
   204   try {
   205     versionInfo.geckoMstone = Services.prefs.getCharPref('gecko.mstone');
   206     versionInfo.geckoBuildID = Services.prefs.getCharPref('gecko.buildID');
   207   } catch (e) {
   208     log('Error encountered while getting platform version info:', e);
   209   }
   210   try {
   211     var addonId = "shumway@research.mozilla.org";
   212     AddonManager.getAddonByID(addonId, function(addon) {
   213       versionInfo.shumwayVersion = addon ? addon.version : 'n/a';
   214       deferred.resolve(versionInfo);
   215     });
   216   } catch (e) {
   217     log('Error encountered while getting Shumway version info:', e);
   218     deferred.resolve(versionInfo);
   219   }
   220   return deferred.promise;
   221 }
   223 function fallbackToNativePlugin(window, userAction, activateCTP) {
   224   var obj = window.frameElement;
   225   var doc = obj.ownerDocument;
   226   var e = doc.createEvent("CustomEvent");
   227   e.initCustomEvent("MozPlayPlugin", true, true, activateCTP);
   228   obj.dispatchEvent(e);
   230   ShumwayTelemetry.onFallback(userAction);
   231 }
   233 // All the priviledged actions.
   234 function ChromeActions(url, window, document) {
   235   this.url = url;
   236   this.objectParams = null;
   237   this.movieParams = null;
   238   this.baseUrl = url;
   239   this.isOverlay = false;
   240   this.isPausedAtStart = false;
   241   this.window = window;
   242   this.document = document;
   243   this.externalComInitialized = false;
   244   this.allowScriptAccess = false;
   245   this.crossdomainRequestsCache = Object.create(null);
   246   this.telemetry = {
   247     startTime: Date.now(),
   248     features: [],
   249     errors: [],
   250     pageIndex: 0
   251   };
   252 }
   254 ChromeActions.prototype = {
   255   getBoolPref: function (data) {
   256     if (!/^shumway\./.test(data.pref)) {
   257       return null;
   258     }
   259     return getBoolPref(data.pref, data.def);
   260   },
   261   getCompilerSettings: function getCompilerSettings() {
   262     return JSON.stringify({
   263       appCompiler: getBoolPref('shumway.appCompiler', true),
   264       sysCompiler: getBoolPref('shumway.sysCompiler', false),
   265       verifier: getBoolPref('shumway.verifier', true)
   266     });
   267   },
   268   addProfilerMarker: function (marker) {
   269     if ('nsIProfiler' in Ci) {
   270       let profiler = Cc["@mozilla.org/tools/profiler;1"].getService(Ci.nsIProfiler);
   271       profiler.AddMarker(marker);
   272     }
   273   },
   274   getPluginParams: function getPluginParams() {
   275     return JSON.stringify({
   276       url: this.url,
   277       baseUrl : this.baseUrl,
   278       movieParams: this.movieParams,
   279       objectParams: this.objectParams,
   280       isOverlay: this.isOverlay,
   281       isPausedAtStart: this.isPausedAtStart
   282      });
   283   },
   284   _canDownloadFile: function canDownloadFile(data, callback) {
   285     var url = data.url, checkPolicyFile = data.checkPolicyFile;
   287     // TODO flash cross-origin request
   288     if (url === this.url) {
   289       // allow downloading for the original file
   290       return callback({success: true});
   291     }
   293     // allows downloading from the same origin
   294     var parsedUrl, parsedBaseUrl;
   295     try {
   296       parsedUrl = NetUtil.newURI(url);
   297     } catch (ex) { /* skipping invalid urls */ }
   298     try {
   299       parsedBaseUrl = NetUtil.newURI(this.url);
   300     } catch (ex) { /* skipping invalid urls */ }
   302     if (parsedUrl && parsedBaseUrl &&
   303         parsedUrl.prePath === parsedBaseUrl.prePath) {
   304       return callback({success: true});
   305     }
   307     // additionally using internal whitelist
   308     var whitelist = getStringPref('shumway.whitelist', '');
   309     if (whitelist && parsedUrl) {
   310       var whitelisted = whitelist.split(',').some(function (i) {
   311         return domainMatches(parsedUrl.host, i);
   312       });
   313       if (whitelisted) {
   314         return callback({success: true});
   315       }
   316     }
   318     if (!checkPolicyFile || !parsedUrl || !parsedBaseUrl) {
   319       return callback({success: false});
   320     }
   322     // we can request crossdomain.xml
   323     fetchPolicyFile(parsedUrl.prePath + '/crossdomain.xml', this.crossdomainRequestsCache,
   324       function (policy, error) {
   326       if (!policy || policy.siteControl === 'none') {
   327         return callback({success: false});
   328       }
   329       // TODO assuming master-only, there are also 'by-content-type', 'all', etc.
   331       var allowed = policy.allowAccessFrom.some(function (i) {
   332         return domainMatches(parsedBaseUrl.host, i.domain) &&
   333           (!i.secure || parsedBaseUrl.scheme.toLowerCase() === 'https');
   334       });
   335       return callback({success: allowed});
   336     }.bind(this));
   337   },
   338   loadFile: function loadFile(data) {
   339     var url = data.url;
   340     var checkPolicyFile = data.checkPolicyFile;
   341     var sessionId = data.sessionId;
   342     var limit = data.limit || 0;
   343     var method = data.method || "GET";
   344     var mimeType = data.mimeType;
   345     var postData = data.postData || null;
   347     var win = this.window;
   348     var baseUrl = this.baseUrl;
   350     var performXHR = function () {
   351       var xhr = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
   352                                   .createInstance(Ci.nsIXMLHttpRequest);
   353       xhr.open(method, url, true);
   354       xhr.responseType = "moz-chunked-arraybuffer";
   356       if (baseUrl) {
   357         // Setting the referer uri, some site doing checks if swf is embedded
   358         // on the original page.
   359         xhr.setRequestHeader("Referer", baseUrl);
   360       }
   362       // TODO apply range request headers if limit is specified
   364       var lastPosition = 0;
   365       xhr.onprogress = function (e) {
   366         var position = e.loaded;
   367         var data = new Uint8Array(xhr.response);
   368         win.postMessage({callback:"loadFile", sessionId: sessionId, topic: "progress",
   369                          array: data, loaded: e.loaded, total: e.total}, "*");
   370         lastPosition = position;
   371         if (limit && e.total >= limit) {
   372           xhr.abort();
   373         }
   374       };
   375       xhr.onreadystatechange = function(event) {
   376         if (xhr.readyState === 4) {
   377           if (xhr.status !== 200 && xhr.status !== 0) {
   378             win.postMessage({callback:"loadFile", sessionId: sessionId, topic: "error",
   379                              error: xhr.statusText}, "*");
   380           }
   381           win.postMessage({callback:"loadFile", sessionId: sessionId, topic: "close"}, "*");
   382         }
   383       };
   384       if (mimeType)
   385         xhr.setRequestHeader("Content-Type", mimeType);
   386       xhr.send(postData);
   387       win.postMessage({callback:"loadFile", sessionId: sessionId, topic: "open"}, "*");
   388     };
   390     this._canDownloadFile({url: url, checkPolicyFile: checkPolicyFile}, function (data) {
   391       if (data.success) {
   392         performXHR();
   393       } else {
   394         log("data access id prohibited to " + url + " from " + baseUrl);
   395         win.postMessage({callback:"loadFile", sessionId: sessionId, topic: "error",
   396           error: "only original swf file or file from the same origin loading supported"}, "*");
   397       }
   398     });
   399   },
   400   fallback: function(automatic) {
   401     automatic = !!automatic;
   402     fallbackToNativePlugin(this.window, !automatic, automatic);
   403   },
   404   setClipboard: function (data) {
   405     if (typeof data !== 'string' ||
   406         data.length > MAX_CLIPBOARD_DATA_SIZE ||
   407         !this.document.hasFocus()) {
   408       return;
   409     }
   410     // TODO other security checks?
   412     let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
   413                       .getService(Ci.nsIClipboardHelper);
   414     clipboard.copyString(data);
   415   },
   416   unsafeSetClipboard: function (data) {
   417     if (typeof data !== 'string') {
   418       return;
   419     }
   420     let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
   421     clipboard.copyString(data);
   422   },
   423   endActivation: function () {
   424     if (ActivationQueue.currentNonActive === this) {
   425       ActivationQueue.activateNext();
   426     }
   427   },
   428   reportTelemetry: function (data) {
   429     var topic = data.topic;
   430     switch (topic) {
   431     case 'firstFrame':
   432       var time = Date.now() - this.telemetry.startTime;
   433       ShumwayTelemetry.onFirstFrame(time);
   434       break;
   435     case 'parseInfo':
   436       ShumwayTelemetry.onParseInfo({
   437         parseTime: +data.parseTime,
   438         size: +data.bytesTotal,
   439         swfVersion: data.swfVersion|0,
   440         frameRate: +data.frameRate,
   441         width: data.width|0,
   442         height: data.height|0,
   443         bannerType: data.bannerType|0,
   444         isAvm2: !!data.isAvm2
   445       });
   446       break;
   447     case 'feature':
   448       var featureType = data.feature|0;
   449       var MIN_FEATURE_TYPE = 0, MAX_FEATURE_TYPE = 999;
   450       if (featureType >= MIN_FEATURE_TYPE && featureType <= MAX_FEATURE_TYPE &&
   451           !this.telemetry.features[featureType]) {
   452         this.telemetry.features[featureType] = true; // record only one feature per SWF
   453         ShumwayTelemetry.onFeature(featureType);
   454       }
   455       break;
   456     case 'error':
   457       var errorType = data.error|0;
   458       var MIN_ERROR_TYPE = 0, MAX_ERROR_TYPE = 2;
   459       if (errorType >= MIN_ERROR_TYPE && errorType <= MAX_ERROR_TYPE &&
   460           !this.telemetry.errors[errorType]) {
   461         this.telemetry.errors[errorType] = true; // record only one report per SWF
   462         ShumwayTelemetry.onError(errorType);
   463       }
   464       break;
   465     }
   466   },
   467   reportIssue: function(exceptions) {
   468     var base = "http://shumway-issue-reporter.paas.allizom.org/input?";
   469     var windowUrl = this.window.parent.wrappedJSObject.location + '';
   470     var params = 'url=' + encodeURIComponent(windowUrl);
   471     params += '&swf=' + encodeURIComponent(this.url);
   472     getVersionInfo().then(function (versions) {
   473       params += '&ffbuild=' + encodeURIComponent(versions.geckoMstone + ' (' +
   474                                                  versions.geckoBuildID + ')');
   475       params += '&shubuild=' + encodeURIComponent(versions.shumwayVersion);
   476     }).then(function () {
   477       var postDataStream = StringInputStream.
   478                            createInstance(Ci.nsIStringInputStream);
   479       postDataStream.data = 'exceptions=' + encodeURIComponent(exceptions);
   480       var postData = MimeInputStream.createInstance(Ci.nsIMIMEInputStream);
   481       postData.addHeader("Content-Type", "application/x-www-form-urlencoded");
   482       postData.addContentLength = true;
   483       postData.setData(postDataStream);
   484       this.window.openDialog('chrome://browser/content', '_blank',
   485                              'all,dialog=no', base + params, null, null,
   486                              postData);
   487     }.bind(this));
   488   },
   489   externalCom: function (data) {
   490     if (!this.allowScriptAccess)
   491       return;
   493     // TODO check security ?
   494     var parentWindow = this.window.parent.wrappedJSObject;
   495     var embedTag = this.embedTag.wrappedJSObject;
   496     switch (data.action) {
   497     case 'init':
   498       if (this.externalComInitialized)
   499         return;
   501       this.externalComInitialized = true;
   502       var eventTarget = this.window.document;
   503       initExternalCom(parentWindow, embedTag, eventTarget);
   504       return;
   505     case 'getId':
   506       return embedTag.id;
   507     case 'eval':
   508       return parentWindow.__flash__eval(data.expression);
   509     case 'call':
   510       return parentWindow.__flash__call(data.request);
   511     case 'register':
   512       return embedTag.__flash__registerCallback(data.functionName);
   513     case 'unregister':
   514       return embedTag.__flash__unregisterCallback(data.functionName);
   515     }
   516   },
   517   getWindowUrl: function() {
   518     return this.window.parent.wrappedJSObject.location + '';
   519   }
   520 };
   522 // Event listener to trigger chrome privedged code.
   523 function RequestListener(actions) {
   524   this.actions = actions;
   525 }
   526 // Receive an event and synchronously or asynchronously responds.
   527 RequestListener.prototype.receive = function(event) {
   528   var message = event.target;
   529   var action = event.detail.action;
   530   var data = event.detail.data;
   531   var sync = event.detail.sync;
   532   var actions = this.actions;
   533   if (!(action in actions)) {
   534     log('Unknown action: ' + action);
   535     return;
   536   }
   537   if (sync) {
   538     var response = actions[action].call(this.actions, data);
   539     var detail = event.detail;
   540     detail.__exposedProps__ = {response: 'r'};
   541     detail.response = response;
   542   } else {
   543     var response;
   544     if (event.detail.callback) {
   545       var cookie = event.detail.cookie;
   546       response = function sendResponse(response) {
   547         var doc = actions.document;
   548         try {
   549           var listener = doc.createEvent('CustomEvent');
   550           listener.initCustomEvent('shumway.response', true, false,
   551                                    {response: response,
   552                                     cookie: cookie,
   553                                     __exposedProps__: {response: 'r', cookie: 'r'}});
   555           return message.dispatchEvent(listener);
   556         } catch (e) {
   557           // doc is no longer accessible because the requestor is already
   558           // gone. unloaded content cannot receive the response anyway.
   559         }
   560       };
   561     }
   562     actions[action].call(this.actions, data, response);
   563   }
   564 };
   566 var ActivationQueue = {
   567   nonActive: [],
   568   initializing: -1,
   569   activationTimeout: null,
   570   get currentNonActive() {
   571     return this.nonActive[this.initializing];
   572   },
   573   enqueue: function ActivationQueue_enqueue(actions) {
   574     this.nonActive.push(actions);
   575     if (this.nonActive.length === 1) {
   576       this.activateNext();
   577     }
   578   },
   579   findLastOnPage: function ActivationQueue_findLastOnPage(baseUrl) {
   580     for (var i = this.nonActive.length - 1; i >= 0; i--) {
   581       if (this.nonActive[i].baseUrl === baseUrl) {
   582         return this.nonActive[i];
   583       }
   584     }
   585     return null;
   586   },
   587   activateNext: function ActivationQueue_activateNext() {
   588     function weightInstance(actions) {
   589       // set of heuristics for find the most important instance to load
   590       var weight = 0;
   591       // using linear distance to the top-left of the view area
   592       if (actions.embedTag) {
   593         var window = actions.window;
   594         var clientRect = actions.embedTag.getBoundingClientRect();
   595         weight -= Math.abs(clientRect.left - window.scrollX) +
   596                   Math.abs(clientRect.top - window.scrollY);
   597       }
   598       var doc = actions.document;
   599       if (!doc.hidden) {
   600         weight += 100000; // might not be that important if hidden
   601       }
   602       if (actions.embedTag &&
   603           actions.embedTag.ownerDocument.hasFocus()) {
   604         weight += 10000; // parent document is focused
   605       }
   606       return weight;
   607     }
   609     if (this.activationTimeout) {
   610       this.activationTimeout.cancel();
   611       this.activationTimeout = null;
   612     }
   614     if (this.initializing >= 0) {
   615       this.nonActive.splice(this.initializing, 1);
   616     }
   617     var weights = [];
   618     for (var i = 0; i < this.nonActive.length; i++) {
   619       try {
   620         var weight = weightInstance(this.nonActive[i]);
   621         weights.push(weight);
   622       } catch (ex) {
   623         // unable to calc weight the instance, removing
   624         log('Shumway instance weight calculation failed: ' + ex);
   625         this.nonActive.splice(i, 1);
   626         i--;
   627       }
   628     }
   630     do {
   631       if (this.nonActive.length === 0) {
   632         this.initializing = -1;
   633         return;
   634       }
   636       var maxWeightIndex = 0;
   637       var maxWeight = weights[0];
   638       for (var i = 1; i < weights.length; i++) {
   639         if (maxWeight < weights[i]) {
   640           maxWeight = weights[i];
   641           maxWeightIndex = i;
   642         }
   643       }
   644       try {
   645         this.initializing = maxWeightIndex;
   646         this.nonActive[maxWeightIndex].activationCallback();
   647         break;
   648       } catch (ex) {
   649         // unable to initialize the instance, trying another one
   650         log('Shumway instance initialization failed: ' + ex);
   651         this.nonActive.splice(maxWeightIndex, 1);
   652         weights.splice(maxWeightIndex, 1);
   653       }
   654     } while (true);
   656     var ACTIVATION_TIMEOUT = 3000;
   657     this.activationTimeout = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   658     this.activationTimeout.initWithCallback(function () {
   659       log('Timeout during shumway instance initialization');
   660       this.activateNext();
   661     }.bind(this), ACTIVATION_TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT);
   662   }
   663 };
   665 function activateShumwayScripts(window, preview) {
   666   function loadScripts(scripts, callback) {
   667     function scriptLoaded() {
   668       leftToLoad--;
   669       if (leftToLoad === 0) {
   670         callback();
   671       }
   672     }
   673     var leftToLoad = scripts.length;
   674     var document = window.document.wrappedJSObject;
   675     var head = document.getElementsByTagName('head')[0];
   676     for (var i = 0; i < scripts.length; i++) {
   677       var script = document.createElement('script');
   678       script.type = "text/javascript";
   679       script.src = scripts[i];
   680       script.onload = scriptLoaded;
   681       head.appendChild(script);
   682     }
   683   }
   685   function initScripts() {
   686     if (preview) {
   687       loadScripts(['resource://shumway/web/preview.js'], function () {
   688         window.wrappedJSObject.runSniffer();
   689       });
   690     } else {
   691       loadScripts(['resource://shumway/shumway.js',
   692                    'resource://shumway/web/avm-sandbox.js'], function () {
   693         window.wrappedJSObject.runViewer();
   694       });
   695     }
   696   }
   698   window.wrappedJSObject.SHUMWAY_ROOT = "resource://shumway/";
   700   if (window.document.readyState === "interactive" ||
   701       window.document.readyState === "complete") {
   702     initScripts();
   703   } else {
   704     window.document.addEventListener('DOMContentLoaded', initScripts);
   705   }
   706 }
   708 function initExternalCom(wrappedWindow, wrappedObject, targetDocument) {
   709   if (!wrappedWindow.__flash__initialized) {
   710     wrappedWindow.__flash__initialized = true;
   711     wrappedWindow.__flash__toXML = function __flash__toXML(obj) {
   712       switch (typeof obj) {
   713       case 'boolean':
   714         return obj ? '<true/>' : '<false/>';
   715       case 'number':
   716         return '<number>' + obj + '</number>';
   717       case 'object':
   718         if (obj === null) {
   719           return '<null/>';
   720         }
   721         if ('hasOwnProperty' in obj && obj.hasOwnProperty('length')) {
   722           // array
   723           var xml = '<array>';
   724           for (var i = 0; i < obj.length; i++) {
   725             xml += '<property id="' + i + '">' + __flash__toXML(obj[i]) + '</property>';
   726           }
   727           return xml + '</array>';
   728         }
   729         var xml = '<object>';
   730         for (var i in obj) {
   731           xml += '<property id="' + i + '">' + __flash__toXML(obj[i]) + '</property>';
   732         }
   733         return xml + '</object>';
   734       case 'string':
   735         return '<string>' + obj.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</string>';
   736       case 'undefined':
   737         return '<undefined/>';
   738       }
   739     };
   740     var sandbox = new Cu.Sandbox(wrappedWindow, {sandboxPrototype: wrappedWindow});
   741     wrappedWindow.__flash__eval = function (evalInSandbox, sandbox, expr) {
   742       this.console.log('__flash__eval: ' + expr);
   743       return evalInSandbox(expr, sandbox);
   744     }.bind(wrappedWindow, Cu.evalInSandbox, sandbox);
   745     wrappedWindow.__flash__call = function (expr) {
   746       this.console.log('__flash__call (ignored): ' + expr);
   747     };
   748   }
   749   wrappedObject.__flash__registerCallback = function (functionName) {
   750     wrappedWindow.console.log('__flash__registerCallback: ' + functionName);
   751     this[functionName] = function () {
   752       var args = Array.prototype.slice.call(arguments, 0);
   753       wrappedWindow.console.log('__flash__callIn: ' + functionName);
   754       var e = targetDocument.createEvent('CustomEvent');
   755       e.initCustomEvent('shumway.remote', true, false, {
   756         functionName: functionName,
   757         args: args,
   758         __exposedProps__: {args: 'r', functionName: 'r', result: 'rw'}
   759       });
   760       targetDocument.dispatchEvent(e);
   761       return e.detail.result;
   762     };
   763   };
   764   wrappedObject.__flash__unregisterCallback = function (functionName) {
   765     wrappedWindow.console.log('__flash__unregisterCallback: ' + functionName);
   766     delete this[functionName];
   767   };
   768 }
   770 function ShumwayStreamConverterBase() {
   771 }
   773 ShumwayStreamConverterBase.prototype = {
   774   QueryInterface: XPCOMUtils.generateQI([
   775       Ci.nsISupports,
   776       Ci.nsIStreamConverter,
   777       Ci.nsIStreamListener,
   778       Ci.nsIRequestObserver
   779   ]),
   781   /*
   782    * This component works as such:
   783    * 1. asyncConvertData stores the listener
   784    * 2. onStartRequest creates a new channel, streams the viewer and cancels
   785    *    the request so Shumway can do the request
   786    * Since the request is cancelled onDataAvailable should not be called. The
   787    * onStopRequest does nothing. The convert function just returns the stream,
   788    * it's just the synchronous version of asyncConvertData.
   789    */
   791   // nsIStreamConverter::convert
   792   convert: function(aFromStream, aFromType, aToType, aCtxt) {
   793     throw Cr.NS_ERROR_NOT_IMPLEMENTED;
   794   },
   796   getUrlHint: function(requestUrl) {
   797     return requestUrl.spec;
   798   },
   800   createChromeActions: function(window, document, urlHint) {
   801     var url = urlHint;
   802     var baseUrl;
   803     var pageUrl;
   804     var element = window.frameElement;
   805     var isOverlay = false;
   806     var objectParams = {};
   807     if (element) {
   808       // PlayPreview overlay "belongs" to the embed/object tag and consists of
   809       // DIV and IFRAME. Starting from IFRAME and looking for first object tag.
   810       var tagName = element.nodeName, containerElement;
   811       while (tagName != 'EMBED' && tagName != 'OBJECT') {
   812         // plugin overlay skipping until the target plugin is found
   813         isOverlay = true;
   814         containerElement = element;
   815         element = element.parentNode;
   816         if (!element) {
   817           throw new Error('Plugin element is not found');
   818         }
   819         tagName = element.nodeName;
   820       }
   822       if (isOverlay) {
   823         // Checking if overlay is a proper PlayPreview overlay.
   824         for (var i = 0; i < element.children.length; i++) {
   825           if (element.children[i] === containerElement) {
   826             throw new Error('Plugin element is invalid');
   827           }
   828         }
   829       }
   830     }
   832     if (element) {
   833       // Getting absolute URL from the EMBED tag
   834       url = element.srcURI.spec;
   836       pageUrl = element.ownerDocument.location.href; // proper page url?
   838       if (tagName == 'EMBED') {
   839         for (var i = 0; i < element.attributes.length; ++i) {
   840           var paramName = element.attributes[i].localName.toLowerCase();
   841           objectParams[paramName] = element.attributes[i].value;
   842         }
   843       } else {
   844         for (var i = 0; i < element.childNodes.length; ++i) {
   845           var paramElement = element.childNodes[i];
   846           if (paramElement.nodeType != 1 ||
   847               paramElement.nodeName != 'PARAM') {
   848             continue;
   849           }
   850           var paramName = paramElement.getAttribute('name').toLowerCase();
   851           objectParams[paramName] = paramElement.getAttribute('value');
   852         }
   853       }
   854     }
   856     if (!url) { // at this point url shall be known -- asserting
   857       throw new Error('Movie url is not specified');
   858     }
   860     baseUrl = objectParams.base || pageUrl;
   862     var movieParams = {};
   863     if (objectParams.flashvars) {
   864       movieParams = parseQueryString(objectParams.flashvars);
   865     }
   866     var queryStringMatch = /\?([^#]+)/.exec(url);
   867     if (queryStringMatch) {
   868       var queryStringParams = parseQueryString(queryStringMatch[1]);
   869       for (var i in queryStringParams) {
   870         if (!(i in movieParams)) {
   871           movieParams[i] = queryStringParams[i];
   872         }
   873       }
   874     }
   876     var allowScriptAccess = false;
   877     switch (objectParams.allowscriptaccess || 'sameDomain') {
   878     case 'always':
   879       allowScriptAccess = true;
   880       break;
   881     case 'never':
   882       allowScriptAccess = false;
   883       break;
   884     default:
   885       if (!pageUrl)
   886         break;
   887       try {
   888         // checking if page is in same domain (? same protocol and port)
   889         allowScriptAccess =
   890           Services.io.newURI('/', null, Services.io.newURI(pageUrl, null, null)).spec ==
   891           Services.io.newURI('/', null, Services.io.newURI(url, null, null)).spec;
   892       } catch (ex) {}
   893       break;
   894     }
   896     var actions = new ChromeActions(url, window, document);
   897     actions.objectParams = objectParams;
   898     actions.movieParams = movieParams;
   899     actions.baseUrl = baseUrl || url;
   900     actions.isOverlay = isOverlay;
   901     actions.embedTag = element;
   902     actions.isPausedAtStart = /\bpaused=true$/.test(urlHint);
   903     actions.allowScriptAccess = allowScriptAccess;
   904     return actions;
   905   },
   907   // nsIStreamConverter::asyncConvertData
   908   asyncConvertData: function(aFromType, aToType, aListener, aCtxt) {
   909     // Store the listener passed to us
   910     this.listener = aListener;
   911   },
   913   // nsIStreamListener::onDataAvailable
   914   onDataAvailable: function(aRequest, aContext, aInputStream, aOffset, aCount) {
   915     // Do nothing since all the data loading is handled by the viewer.
   916     log('SANITY CHECK: onDataAvailable SHOULD NOT BE CALLED!');
   917   },
   919   // nsIRequestObserver::onStartRequest
   920   onStartRequest: function(aRequest, aContext) {
   921     // Setup the request so we can use it below.
   922     aRequest.QueryInterface(Ci.nsIChannel);
   924     aRequest.QueryInterface(Ci.nsIWritablePropertyBag);
   926     // Change the content type so we don't get stuck in a loop.
   927     aRequest.setProperty('contentType', aRequest.contentType);
   928     aRequest.contentType = 'text/html';
   930     // TODO For now suspending request, however we can continue fetching data
   931     aRequest.suspend();
   933     var originalURI = aRequest.URI;
   935     // checking if the plug-in shall be run in simple mode
   936     var isSimpleMode = originalURI.spec === EXPECTED_PLAYPREVIEW_URI_PREFIX &&
   937                        getBoolPref('shumway.simpleMode', false);
   939     // Create a new channel that loads the viewer as a resource.
   940     var viewerUrl = isSimpleMode ?
   941                     'resource://shumway/web/simple.html' :
   942                     'resource://shumway/web/viewer.html';
   943     var channel = Services.io.newChannel(viewerUrl, null, null);
   945     var converter = this;
   946     var listener = this.listener;
   947     // Proxy all the request observer calls, when it gets to onStopRequest
   948     // we can get the dom window.
   949     var proxy = {
   950       onStartRequest: function(request, context) {
   951         listener.onStartRequest(aRequest, context);
   952       },
   953       onDataAvailable: function(request, context, inputStream, offset, count) {
   954         listener.onDataAvailable(aRequest, context, inputStream, offset, count);
   955       },
   956       onStopRequest: function(request, context, statusCode) {
   957         // Cancel the request so the viewer can handle it.
   958         aRequest.resume();
   959         aRequest.cancel(Cr.NS_BINDING_ABORTED);
   961         var domWindow = getDOMWindow(channel);
   962         let actions = converter.createChromeActions(domWindow,
   963                                                     domWindow.document,
   964                                                     converter.getUrlHint(originalURI));
   966         if (!isShumwayEnabledFor(actions)) {
   967           fallbackToNativePlugin(domWindow, false, true);
   968           return;
   969         }
   971         // Report telemetry on amount of swfs on the page
   972         if (actions.isOverlay) {
   973           // Looking for last actions with same baseUrl
   974           var prevPageActions = ActivationQueue.findLastOnPage(actions.baseUrl);
   975           var pageIndex = !prevPageActions ? 1 : (prevPageActions.telemetry.pageIndex + 1);
   976           actions.telemetry.pageIndex = pageIndex;
   977           ShumwayTelemetry.onPageIndex(pageIndex);
   978         } else {
   979           ShumwayTelemetry.onPageIndex(0);
   980         }
   982         actions.activationCallback = function(domWindow, isSimpleMode) {
   983           delete this.activationCallback;
   984           activateShumwayScripts(domWindow, isSimpleMode);
   985         }.bind(actions, domWindow, isSimpleMode);
   986         ActivationQueue.enqueue(actions);
   988         let requestListener = new RequestListener(actions);
   989         domWindow.addEventListener('shumway.message', function(event) {
   990           requestListener.receive(event);
   991         }, false, true);
   993         listener.onStopRequest(aRequest, context, statusCode);
   994       }
   995     };
   997     // Keep the URL the same so the browser sees it as the same.
   998     channel.originalURI = aRequest.URI;
   999     channel.loadGroup = aRequest.loadGroup;
  1001     // We can use resource principal when data is fetched by the chrome
  1002     // e.g. useful for NoScript
  1003     var securityManager = Cc['@mozilla.org/scriptsecuritymanager;1']
  1004                           .getService(Ci.nsIScriptSecurityManager);
  1005     var uri = Services.io.newURI(viewerUrl, null, null);
  1006     var resourcePrincipal = securityManager.getNoAppCodebasePrincipal(uri);
  1007     aRequest.owner = resourcePrincipal;
  1008     channel.asyncOpen(proxy, aContext);
  1009   },
  1011   // nsIRequestObserver::onStopRequest
  1012   onStopRequest: function(aRequest, aContext, aStatusCode) {
  1013     // Do nothing.
  1015 };
  1017 // properties required for XPCOM registration:
  1018 function copyProperties(obj, template) {
  1019   for (var prop in template) {
  1020     obj[prop] = template[prop];
  1024 function ShumwayStreamConverter() {}
  1025 ShumwayStreamConverter.prototype = new ShumwayStreamConverterBase();
  1026 copyProperties(ShumwayStreamConverter.prototype, {
  1027   classID: Components.ID('{4c6030f7-e20a-264f-5b0e-ada3a9e97384}'),
  1028   classDescription: 'Shumway Content Converter Component',
  1029   contractID: '@mozilla.org/streamconv;1?from=application/x-shockwave-flash&to=*/*'
  1030 });
  1032 function ShumwayStreamOverlayConverter() {}
  1033 ShumwayStreamOverlayConverter.prototype = new ShumwayStreamConverterBase();
  1034 copyProperties(ShumwayStreamOverlayConverter.prototype, {
  1035   classID: Components.ID('{4c6030f7-e20a-264f-5f9b-ada3a9e97384}'),
  1036   classDescription: 'Shumway PlayPreview Component',
  1037   contractID: '@mozilla.org/streamconv;1?from=application/x-moz-playpreview&to=*/*'
  1038 });
  1039 ShumwayStreamOverlayConverter.prototype.getUrlHint = function (requestUrl) {
  1040   return '';
  1041 };

mercurial