browser/extensions/shumway/content/ShumwayStreamConverter.jsm

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:c872de7476d3
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 */
18
19 'use strict';
20
21 var EXPORTED_SYMBOLS = ['ShumwayStreamConverter', 'ShumwayStreamOverlayConverter'];
22
23 const Cc = Components.classes;
24 const Ci = Components.interfaces;
25 const Cr = Components.results;
26 const Cu = Components.utils;
27
28 const SHUMWAY_CONTENT_TYPE = 'application/x-shockwave-flash';
29 const EXPECTED_PLAYPREVIEW_URI_PREFIX = 'data:application/x-moz-playpreview;,' +
30 SHUMWAY_CONTENT_TYPE;
31
32 const FIREFOX_ID = '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}';
33 const SEAMONKEY_ID = '{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}';
34
35 const MAX_CLIPBOARD_DATA_SIZE = 8000;
36
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');
41
42 XPCOMUtils.defineLazyModuleGetter(this, 'PrivateBrowsingUtils',
43 'resource://gre/modules/PrivateBrowsingUtils.jsm');
44
45 XPCOMUtils.defineLazyModuleGetter(this, 'AddonManager',
46 'resource://gre/modules/AddonManager.jsm');
47
48 XPCOMUtils.defineLazyModuleGetter(this, 'ShumwayTelemetry',
49 'resource://shumway/ShumwayTelemetry.jsm');
50
51 let Svc = {};
52 XPCOMUtils.defineLazyServiceGetter(Svc, 'mime',
53 '@mozilla.org/mime;1', 'nsIMIMEService');
54
55 let StringInputStream = Cc["@mozilla.org/io/string-input-stream;1"];
56 let MimeInputStream = Cc["@mozilla.org/network/mime-input-stream;1"];
57
58 function getBoolPref(pref, def) {
59 try {
60 return Services.prefs.getBoolPref(pref);
61 } catch (ex) {
62 return def;
63 }
64 }
65
66 function getStringPref(pref, def) {
67 try {
68 return Services.prefs.getComplexValue(pref, Ci.nsISupportsString).data;
69 } catch (ex) {
70 return def;
71 }
72 }
73
74 function log(aMsg) {
75 let msg = 'ShumwayStreamConverter.js: ' + (aMsg.join ? aMsg.join('') : aMsg);
76 Services.console.logStringMessage(msg);
77 dump(msg + '\n');
78 }
79
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 }
86
87 function parseQueryString(qs) {
88 if (!qs)
89 return {};
90
91 if (qs.charAt(0) == '?')
92 qs = qs.slice(1);
93
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 }
101
102 return obj;
103 }
104
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 }
119
120 function fetchPolicyFile(url, cache, callback) {
121 if (url in cache) {
122 return callback(cache[url]);
123 }
124
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 }
171
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 }
181
182 var url = actions.url;
183 var baseUrl = actions.baseUrl;
184
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 }
193
194 return true;
195 }
196
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 }
222
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);
229
230 ShumwayTelemetry.onFallback(userAction);
231 }
232
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 }
253
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;
286
287 // TODO flash cross-origin request
288 if (url === this.url) {
289 // allow downloading for the original file
290 return callback({success: true});
291 }
292
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 */ }
301
302 if (parsedUrl && parsedBaseUrl &&
303 parsedUrl.prePath === parsedBaseUrl.prePath) {
304 return callback({success: true});
305 }
306
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 }
317
318 if (!checkPolicyFile || !parsedUrl || !parsedBaseUrl) {
319 return callback({success: false});
320 }
321
322 // we can request crossdomain.xml
323 fetchPolicyFile(parsedUrl.prePath + '/crossdomain.xml', this.crossdomainRequestsCache,
324 function (policy, error) {
325
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.
330
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;
346
347 var win = this.window;
348 var baseUrl = this.baseUrl;
349
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";
355
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 }
361
362 // TODO apply range request headers if limit is specified
363
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 };
389
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?
411
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;
492
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;
500
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 };
521
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'}});
554
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 };
565
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 }
608
609 if (this.activationTimeout) {
610 this.activationTimeout.cancel();
611 this.activationTimeout = null;
612 }
613
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 }
629
630 do {
631 if (this.nonActive.length === 0) {
632 this.initializing = -1;
633 return;
634 }
635
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);
655
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 };
664
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 }
684
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 }
697
698 window.wrappedJSObject.SHUMWAY_ROOT = "resource://shumway/";
699
700 if (window.document.readyState === "interactive" ||
701 window.document.readyState === "complete") {
702 initScripts();
703 } else {
704 window.document.addEventListener('DOMContentLoaded', initScripts);
705 }
706 }
707
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 }
769
770 function ShumwayStreamConverterBase() {
771 }
772
773 ShumwayStreamConverterBase.prototype = {
774 QueryInterface: XPCOMUtils.generateQI([
775 Ci.nsISupports,
776 Ci.nsIStreamConverter,
777 Ci.nsIStreamListener,
778 Ci.nsIRequestObserver
779 ]),
780
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 */
790
791 // nsIStreamConverter::convert
792 convert: function(aFromStream, aFromType, aToType, aCtxt) {
793 throw Cr.NS_ERROR_NOT_IMPLEMENTED;
794 },
795
796 getUrlHint: function(requestUrl) {
797 return requestUrl.spec;
798 },
799
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 }
821
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 }
831
832 if (element) {
833 // Getting absolute URL from the EMBED tag
834 url = element.srcURI.spec;
835
836 pageUrl = element.ownerDocument.location.href; // proper page url?
837
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 }
855
856 if (!url) { // at this point url shall be known -- asserting
857 throw new Error('Movie url is not specified');
858 }
859
860 baseUrl = objectParams.base || pageUrl;
861
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 }
875
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 }
895
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 },
906
907 // nsIStreamConverter::asyncConvertData
908 asyncConvertData: function(aFromType, aToType, aListener, aCtxt) {
909 // Store the listener passed to us
910 this.listener = aListener;
911 },
912
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 },
918
919 // nsIRequestObserver::onStartRequest
920 onStartRequest: function(aRequest, aContext) {
921 // Setup the request so we can use it below.
922 aRequest.QueryInterface(Ci.nsIChannel);
923
924 aRequest.QueryInterface(Ci.nsIWritablePropertyBag);
925
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';
929
930 // TODO For now suspending request, however we can continue fetching data
931 aRequest.suspend();
932
933 var originalURI = aRequest.URI;
934
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);
938
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);
944
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);
960
961 var domWindow = getDOMWindow(channel);
962 let actions = converter.createChromeActions(domWindow,
963 domWindow.document,
964 converter.getUrlHint(originalURI));
965
966 if (!isShumwayEnabledFor(actions)) {
967 fallbackToNativePlugin(domWindow, false, true);
968 return;
969 }
970
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 }
981
982 actions.activationCallback = function(domWindow, isSimpleMode) {
983 delete this.activationCallback;
984 activateShumwayScripts(domWindow, isSimpleMode);
985 }.bind(actions, domWindow, isSimpleMode);
986 ActivationQueue.enqueue(actions);
987
988 let requestListener = new RequestListener(actions);
989 domWindow.addEventListener('shumway.message', function(event) {
990 requestListener.receive(event);
991 }, false, true);
992
993 listener.onStopRequest(aRequest, context, statusCode);
994 }
995 };
996
997 // Keep the URL the same so the browser sees it as the same.
998 channel.originalURI = aRequest.URI;
999 channel.loadGroup = aRequest.loadGroup;
1000
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 },
1010
1011 // nsIRequestObserver::onStopRequest
1012 onStopRequest: function(aRequest, aContext, aStatusCode) {
1013 // Do nothing.
1014 }
1015 };
1016
1017 // properties required for XPCOM registration:
1018 function copyProperties(obj, template) {
1019 for (var prop in template) {
1020 obj[prop] = template[prop];
1021 }
1022 }
1023
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 });
1031
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