|
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 |
|
20 // Extenstion communication object |
|
21 var FirefoxCom = (function FirefoxComClosure() { |
|
22 return { |
|
23 /** |
|
24 * Creates an event that the extension is listening for and will |
|
25 * synchronously respond to. |
|
26 * NOTE: It is reccomended to use request() instead since one day we may not |
|
27 * be able to synchronously reply. |
|
28 * @param {String} action The action to trigger. |
|
29 * @param {String} data Optional data to send. |
|
30 * @return {*} The response. |
|
31 */ |
|
32 requestSync: function(action, data) { |
|
33 var request = document.createTextNode(''); |
|
34 document.documentElement.appendChild(request); |
|
35 |
|
36 var sender = document.createEvent('CustomEvent'); |
|
37 sender.initCustomEvent('shumway.message', true, false, |
|
38 {action: action, data: data, sync: true}); |
|
39 request.dispatchEvent(sender); |
|
40 var response = sender.detail.response; |
|
41 document.documentElement.removeChild(request); |
|
42 return response; |
|
43 }, |
|
44 /** |
|
45 * Creates an event that the extension is listening for and will |
|
46 * asynchronously respond by calling the callback. |
|
47 * @param {String} action The action to trigger. |
|
48 * @param {String} data Optional data to send. |
|
49 * @param {Function} callback Optional response callback that will be called |
|
50 * with one data argument. |
|
51 */ |
|
52 request: function(action, data, callback) { |
|
53 var request = document.createTextNode(''); |
|
54 request.setUserData('action', action, null); |
|
55 request.setUserData('data', data, null); |
|
56 request.setUserData('sync', false, null); |
|
57 if (callback) { |
|
58 request.setUserData('callback', callback, null); |
|
59 |
|
60 document.addEventListener('shumway.response', function listener(event) { |
|
61 var node = event.target, |
|
62 response = event.detail.response; |
|
63 |
|
64 document.documentElement.removeChild(node); |
|
65 |
|
66 document.removeEventListener('shumway.response', listener, false); |
|
67 return callback(response); |
|
68 }, false); |
|
69 } |
|
70 document.documentElement.appendChild(request); |
|
71 |
|
72 var sender = document.createEvent('CustomEvent'); |
|
73 sender.initCustomEvent('shumway.message', true, false, |
|
74 {action: action, data: data, sync: false}); |
|
75 return request.dispatchEvent(sender); |
|
76 } |
|
77 }; |
|
78 })(); |
|
79 |
|
80 function fallback() { |
|
81 FirefoxCom.requestSync('fallback', null) |
|
82 } |
|
83 |
|
84 var BYTES_TO_LOAD = 32768; |
|
85 var BYTES_TO_PARSE = 32768; |
|
86 |
|
87 function runSniffer() { |
|
88 var flashParams = JSON.parse(FirefoxCom.requestSync('getPluginParams', null)); |
|
89 document.head.getElementsByTagName('base')[0].href = flashParams.baseUrl; |
|
90 movieUrl = flashParams.url; |
|
91 document.getElementById('playbutton').addEventListener('click', function() { |
|
92 switchToFullMode(); |
|
93 }); |
|
94 document.getElementById('fullmode').addEventListener('click', function() { |
|
95 switchToFullMode(); |
|
96 return false; |
|
97 }); |
|
98 document.getElementById('fallback').addEventListener('click', function() { |
|
99 fallback(); |
|
100 return false; |
|
101 }); |
|
102 FirefoxCom.requestSync('loadFile', {url: movieUrl, sessionId: 0, limit: BYTES_TO_LOAD}); |
|
103 } |
|
104 |
|
105 var subscription, movieUrl, buffers = [];; |
|
106 |
|
107 addEventListener("message", function handlerMessage(e) { |
|
108 var args = e.data; |
|
109 switch (args.callback) { |
|
110 case "loadFile": |
|
111 if (args.sessionId != 0) { |
|
112 return; |
|
113 } |
|
114 switch (args.topic) { |
|
115 case "progress": |
|
116 buffers.push(args.array); |
|
117 break; |
|
118 case "error": |
|
119 console.error('Unable to download ' + movieUrl + ': ' + args.error); |
|
120 break; |
|
121 case "close": |
|
122 parseSwf(); |
|
123 break; |
|
124 } |
|
125 break; |
|
126 } |
|
127 }, true); |
|
128 |
|
129 function inflateData(bytes, outputLength) { |
|
130 verifyDeflateHeader(bytes); |
|
131 var stream = new Stream(bytes, 2); |
|
132 var output = { |
|
133 data: new Uint8Array(outputLength), |
|
134 available: 0, |
|
135 completed: false |
|
136 }; |
|
137 var state = {}; |
|
138 // inflate while we can |
|
139 try { |
|
140 do { |
|
141 inflateBlock(stream, output, state); |
|
142 } while (!output.completed && stream.pos < stream.end |
|
143 && output.available < outputLength); |
|
144 } catch (e) { |
|
145 console.log('inflate aborted: ' + e); |
|
146 } |
|
147 return new Stream(output.data, 0, Math.min(output.available, outputLength)); |
|
148 } |
|
149 |
|
150 function parseSwf() { |
|
151 var sum = 0; |
|
152 for (var i = 0; i < buffers.length; i++) |
|
153 sum += buffers[i].length; |
|
154 var data = new Uint8Array(sum), j = 0; |
|
155 for (var i = 0; i < buffers.length; i++) { |
|
156 data.set(buffers[i], j); j += buffers[i].length; |
|
157 } |
|
158 |
|
159 var backgroundColor; |
|
160 try { |
|
161 var magic1 = data[0]; |
|
162 var magic2 = data[1]; |
|
163 var magic3 = data[2]; |
|
164 if ((magic1 !== 70 && magic1 !== 67) || magic2 !== 87 || magic3 !== 83) |
|
165 throw new Error('unsupported file format'); |
|
166 |
|
167 var compressed = magic1 === 67; |
|
168 var stream = compressed ? inflateData(data.subarray(8), BYTES_TO_PARSE) : |
|
169 new Stream(data, 8, data.length - 8); |
|
170 var bytes = stream.bytes; |
|
171 |
|
172 var SWF_TAG_CODE_SET_BACKGROUND_COLOR = 9; |
|
173 var PREFETCH_SIZE = 17 /* RECT */ + |
|
174 4 /* Frames rate and count */;; |
|
175 stream.ensure(PREFETCH_SIZE); |
|
176 var rectFieldSize = bytes[stream.pos] >> 3; |
|
177 stream.pos += ((5 + 4 * rectFieldSize + 7) >> 3) + 4; // skipping other header fields |
|
178 |
|
179 // for now just sniffing background color |
|
180 while (stream.pos < stream.end && |
|
181 !backgroundColor) { |
|
182 stream.ensure(2); |
|
183 var tagCodeAndLength = stream.getUint16(stream.pos, true); |
|
184 stream.pos += 2; |
|
185 var tagCode = tagCodeAndLength >> 6; |
|
186 var length = tagCodeAndLength & 0x3F; |
|
187 if (length == 0x3F) { |
|
188 stream.ensure(4); |
|
189 length = stream.getInt32(stream.pos, true); |
|
190 stream.pos += 4; |
|
191 if (length < 0) throw new Error('invalid length'); |
|
192 } |
|
193 stream.ensure(length); |
|
194 switch (tagCode) { |
|
195 case SWF_TAG_CODE_SET_BACKGROUND_COLOR: |
|
196 backgroundColor = 'rgb(' + bytes[stream.pos] + ', ' + |
|
197 bytes[stream.pos + 1] + ', ' + |
|
198 bytes[stream.pos + 2] + ')'; |
|
199 break; |
|
200 } |
|
201 stream.pos += length; |
|
202 } |
|
203 } catch (e) { |
|
204 console.log('parsing aborted: ' + e); |
|
205 } |
|
206 if (backgroundColor) { |
|
207 document.body.style.backgroundColor = backgroundColor; |
|
208 } |
|
209 } |
|
210 |
|
211 document.addEventListener('keydown', function (e) { |
|
212 if (e.keyCode == 119 && e.ctrlKey) { // Ctrl+F8 |
|
213 window.location.replace("data:application/x-moz-playpreview;,application/x-shockwave-flash,full,paused=true"); |
|
214 } |
|
215 }, false); |
|
216 |
|
217 function switchToFullMode() { |
|
218 window.location.replace("data:application/x-moz-playpreview;,application/x-shockwave-flash,full"); |
|
219 } |