background.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. /*
  2. * Copyright 2010 Gildas Lormeau
  3. * contact : gildas.lormeau <at> gmail.com
  4. *
  5. * This file is part of SingleFile.
  6. *
  7. * SingleFile is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU Lesser General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * SingleFile is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU Lesser General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU Lesser General Public License
  18. * along with SingleFile. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. var singlefile = {};
  21. (function() {
  22. var dev = false;
  23. var tabs = [], SCRAPBOOK_EXT_ID = dev ? "imfajgkkpglkdjkjejkefllgajgmhmfp" : "ihkkeoeinpbomhnpkmmkpggkaefincbn";
  24. function detectScrapbook(callback) {
  25. var img = new Image();
  26. img.src = "chrome-extension://" + SCRAPBOOK_EXT_ID + "/resources/icon_16.png";
  27. img.onload = function() {
  28. callback(true);
  29. };
  30. img.onerror = function() {
  31. callback(false);
  32. };
  33. }
  34. singlefile.getOptions = function() {
  35. return localStorage["options"] ? JSON.parse(localStorage["options"]) : {
  36. removeFrames : false,
  37. removeScripts : true,
  38. removeObjects : true,
  39. removeHidden : false,
  40. removeUnused : false
  41. };
  42. };
  43. singlefile.setOptions = function(options) {
  44. localStorage["options"] = JSON.stringify(options);
  45. };
  46. singlefile.resetOptions = function() {
  47. delete localStorage["options"];
  48. };
  49. function getWinProperties() {
  50. var winProperties = {}, property;
  51. for (property in window)
  52. winProperties[property] = true;
  53. return winProperties;
  54. }
  55. function throwAwayHighOrderBytes(str) {
  56. var i, ret = [];
  57. for (i = 0; i < str.length; i++)
  58. ret[i] = String.fromCharCode(str.charCodeAt(i) & 0xff);
  59. return ret.join("");
  60. }
  61. function sendXHR(tabId, url, responseHandler, errorHandler, charset) {
  62. var xhr;
  63. if (!tabs[tabId].responses[url]) {
  64. if (!tabs[tabId].callbacks[url]) {
  65. tabs[tabId].callbacks[url] = [];
  66. xhr = new XMLHttpRequest();
  67. xhr.onreadystatechange = function() {
  68. if (xhr.readyState == 4) {
  69. tabs[tabId].callbacks[url].forEach(function(callback) {
  70. callback.responseHandler(xhr.status, xhr.getResponseHeader("Content-Type"), xhr.responseText);
  71. });
  72. tabs[tabId].responses[url] = {
  73. status : xhr.status,
  74. header : xhr.getResponseHeader("Content-Type"),
  75. text : xhr.responseText
  76. };
  77. }
  78. };
  79. xhr.onerror = function() {
  80. tabs[tabId].callbacks[url].forEach(function(callback) {
  81. callback.errorHandler();
  82. });
  83. };
  84. xhr.open("GET", url, true);
  85. if (charset)
  86. xhr.overrideMimeType('text/plain; charset=' + charset);
  87. try {
  88. xhr.send(null);
  89. } catch (e) {
  90. xhr.onerror();
  91. }
  92. }
  93. tabs[tabId].callbacks[url].push({
  94. responseHandler : responseHandler,
  95. errorHandler : errorHandler
  96. });
  97. } else
  98. responseHandler(tabs[tabId].responses[url].status, tabs[tabId].responses[url].header, tabs[tabId].responses[url].text);
  99. }
  100. function setData(tabId, data, callback) {
  101. if (data.url.indexOf('http:') == 0 || data.url.indexOf('https:') == 0)
  102. sendXHR(tabId, data.url, function(status, contentType, responseText) {
  103. if (status < 400) {
  104. data.mediaType = contentType ? contentType.split(";")[0] : null;
  105. data.mediaTypeParam = data.base64 ? "base64" : (contentType ? contentType.split(";")[1] : null);
  106. data.content = data.base64 ? btoa(throwAwayHighOrderBytes(responseText)) : data.encodedText ? encodeURI(responseText) : responseText;
  107. }
  108. callback(data);
  109. }, function() {
  110. callback(data);
  111. }, data.characterSet);
  112. else
  113. callback(data);
  114. }
  115. function startDone(tabId) {
  116. var msg, options = singlefile.getOptions();
  117. msg = {
  118. getStylesheets : true,
  119. options : singlefile.getOptions()
  120. };
  121. tabs[tabId].ports.forEach(function(portData) {
  122. portData.port.postMessage(msg);
  123. });
  124. }
  125. function getPortData(tabId, port) {
  126. var portData;
  127. tabs[tabId].ports.forEach(function(aPortData) {
  128. if (!portData && aPortData.port == port)
  129. portData = aPortData;
  130. });
  131. return portData;
  132. }
  133. function buildTree(tabId) {
  134. var tabData = tabs[tabId];
  135. function findParent(port) {
  136. var parentPort, parts, parentWinID;
  137. if (port.winID) {
  138. parts = port.winID.split('.');
  139. parts.pop();
  140. parentWinID = parts.join('.');
  141. tabData.ports.forEach(function(portData) {
  142. if (portData.winID == parentWinID)
  143. parentPort = portData;
  144. });
  145. }
  146. return parentPort;
  147. }
  148. function walk(portData, level) {
  149. if (portData.children)
  150. portData.children.forEach(function(pData) {
  151. walk(pData, level + 1);
  152. });
  153. if (!tabData.levels[level])
  154. tabData.levels[level] = [];
  155. tabData.levels[level].push(portData);
  156. }
  157. tabData.ports.forEach(function(portData) {
  158. portData.parent = findParent(portData);
  159. if (portData.parent) {
  160. portData.parent.children = portData.parent.children || [];
  161. portData.parent.children.push(portData);
  162. }
  163. });
  164. tabData.levels = [];
  165. walk(tabData.top, 0);
  166. tabData.levelIndex = tabData.levels.length - 1;
  167. }
  168. function processFrames(tabId) {
  169. function postGetDocData(pData) {
  170. var data = {};
  171. if (pData.children)
  172. pData.children.forEach(function(pChildData) {
  173. var index = pChildData.winID.split('.').pop();
  174. data[index] = pChildData.data;
  175. });
  176. pData.port.postMessage({
  177. getDocData : true,
  178. data : data
  179. });
  180. }
  181. tabs[tabId].processedFrameCount = 0;
  182. tabs[tabId].processedFrameMax = tabs[tabId].levels[tabs[tabId].levelIndex].length;
  183. tabs[tabId].levels[tabs[tabId].levelIndex].forEach(postGetDocData);
  184. }
  185. function done(tabId) {
  186. tabs[tabId].processing = false;
  187. tabs[tabId].processed = true;
  188. tabs[tabId].top.port.postMessage({
  189. done : true,
  190. winProperties : getWinProperties()
  191. });
  192. }
  193. function refreshBadge(tabId) {
  194. var processedResources = 0, totalResources = 0;
  195. tabs[tabId].ports.forEach(function(portData) {
  196. processedResources += portData.processedResources;
  197. totalResources += portData.totalResources;
  198. });
  199. chrome.browserAction.setIcon({
  200. tabId : tabId,
  201. path : '../resources/icon_48_wait' + Math.floor((processedResources / totalResources) * 9) + '.png'
  202. });
  203. chrome.browserAction.setTitle({
  204. tabId : tabId,
  205. title : "Progress: " + Math.min(100, Math.floor((processedResources / totalResources) * 100)) + "%"
  206. });
  207. }
  208. function processTab(tabId, scrapbooking) {
  209. function executeScripts(scripts, callback, index) {
  210. if (!index)
  211. index = 0;
  212. if (index < scripts.length) {
  213. chrome.tabs.executeScript(tabId, {
  214. file : scripts[index].file,
  215. code : scripts[index].code,
  216. allFrames : true
  217. }, function() {
  218. executeScripts(scripts, callback, index + 1);
  219. });
  220. } else if (callback)
  221. callback();
  222. }
  223. if (!tabs[tabId] || (!tabs[tabId].processing && !tabs[tabId].processed)) {
  224. executeScripts([ {
  225. code : "var singlefile = {};"
  226. }, {
  227. file : "scripts/filters.js"
  228. }, {
  229. file : "scripts/core.js"
  230. }, {
  231. file : "scripts/ui.js"
  232. }, {
  233. file : "scripts/main.js"
  234. } ], function() {
  235. if (!tabs[tabId])
  236. return;
  237. tabs[tabId].processing = true;
  238. chrome.browserAction.setBadgeBackgroundColor({
  239. color : [ 200, 200, 200, 192 ]
  240. });
  241. chrome.browserAction.setIcon({
  242. tabId : tabId,
  243. path : '../resources/icon_48_wait0.png'
  244. });
  245. chrome.browserAction.setTitle({
  246. tabId : tabId,
  247. title : "Progress: 0%"
  248. });
  249. detectScrapbook(function(sendContent) {
  250. tabs[tabId].top.port.postMessage({
  251. start : true,
  252. sendContent : sendContent,
  253. scrapbooking : scrapbooking
  254. });
  255. });
  256. });
  257. }
  258. }
  259. chrome.browserAction.onClicked.addListener(function(tab) {
  260. processTab(tab.id);
  261. });
  262. chrome.extension.onConnect.addListener(function(port) {
  263. var tabId = port.sender.tab.id, tabData;
  264. port.onDisconnect.addListener(function() {
  265. tabData.ports = tabData.ports.filter(function(portData) {
  266. return portData.port != port;
  267. });
  268. tabData.processedDocMax--;
  269. if (tabData.processedDocCount && tabData.processedDocMax == tabData.processedDocCount) {
  270. tabData.processedDocCount = 0;
  271. buildTree(tabId);
  272. processFrames(tabId);
  273. }
  274. if (!tabData.ports.length)
  275. tabs[tabId] = null;
  276. });
  277. port.onMessage.addListener(function(msg) {
  278. var portData;
  279. if (msg.init) {
  280. tabs[tabId] = tabs[tabId] || {
  281. id : tabId,
  282. ports : [],
  283. processedDocCount : 0,
  284. processedDocMax : 0,
  285. processedPortCount : 0,
  286. processedFrameCount : 0,
  287. processedFrameMax : 0,
  288. processed : false,
  289. callbacks : {},
  290. responses : {}
  291. };
  292. tabData = tabs[tabId];
  293. portData = {
  294. port : port,
  295. url : msg.url,
  296. totalResources : 0,
  297. processedResources : 0
  298. };
  299. if (msg.topWindow) {
  300. tabData.top = portData;
  301. tabData.title = msg.title;
  302. }
  303. if (!singlefile.getOptions().removeFrames || msg.topWindow) {
  304. tabData.ports.push(portData);
  305. tabData.processedDocMax++;
  306. }
  307. } else
  308. portData = getPortData(tabId, port);
  309. if (msg.startDone)
  310. startDone(tabId);
  311. if (msg.setStylesheets || msg.setData || msg.setDynamicData)
  312. setData(tabId, msg.data, function(data) {
  313. msg.data = data;
  314. port.postMessage(msg);
  315. });
  316. if (msg.setTotalResources) {
  317. portData.totalResources = msg.totalResources;
  318. tabData.processedPortCount++;
  319. if (tabData.processedPortCount == tabData.ports.length) {
  320. tabData.ports.forEach(function(pData) {
  321. pData.port.postMessage({
  322. getData : true
  323. });
  324. });
  325. }
  326. }
  327. if (msg.incProcessedResources) {
  328. portData.processedResources++;
  329. refreshBadge(tabId);
  330. }
  331. if (msg.setDataDone) {
  332. portData.frameCount = msg.frameCount;
  333. portData.winID = msg.winID;
  334. if (singlefile.getOptions().removeFrames) {
  335. done(tabId);
  336. detectScrapbook(function(sendContent) {
  337. if (sendContent)
  338. chrome.extension.sendRequest(SCRAPBOOK_EXT_ID, {
  339. tabId : tabId,
  340. title : tabData.title,
  341. content : msg.data,
  342. favicoData : msg.favicoData,
  343. url : tabData.top.url
  344. });
  345. });
  346. } else {
  347. tabData.processedDocCount++;
  348. if (tabData.processedDocMax == tabData.processedDocCount) {
  349. tabData.processedDocCount = 0;
  350. buildTree(tabId);
  351. processFrames(tabId);
  352. }
  353. }
  354. }
  355. if (msg.setDocData) {
  356. portData.data = "data:" + msg.mimeType + ";charset=utf-8," + encodeURI(msg.data);
  357. if (portData.parent) {
  358. tabData.processedFrameCount++;
  359. if (tabData.processedFrameMax == tabData.processedFrameCount) {
  360. tabData.levelIndex--;
  361. processFrames(tabId);
  362. }
  363. } else {
  364. done(tabId);
  365. detectScrapbook(function(sendContent) {
  366. if (sendContent)
  367. chrome.extension.sendRequest(SCRAPBOOK_EXT_ID, {
  368. tabId : tabId,
  369. title : tabData.title,
  370. content : msg.data,
  371. favicoData : msg.favicoData,
  372. url : tabData.top.url
  373. });
  374. });
  375. }
  376. }
  377. if (msg.done) {
  378. chrome.browserAction.setIcon({
  379. tabId : tabId,
  380. path : '../resources/icon_48.png'
  381. });
  382. chrome.browserAction.setBadgeBackgroundColor({
  383. color : [ 10, 200, 10, 192 ]
  384. });
  385. chrome.browserAction.setBadgeText({
  386. tabId : tabId,
  387. text : "OK"
  388. });
  389. chrome.browserAction.setTitle({
  390. tabId : tabId,
  391. title : "Save the page with Ctrl-S"
  392. });
  393. }
  394. });
  395. });
  396. chrome.extension.onRequestExternal.addListener(function(request, sender, sendResponse) {
  397. var tabId = request.id;
  398. if (sender.id != SCRAPBOOK_EXT_ID)
  399. return;
  400. chrome.browserAction.setBadgeText({
  401. tabId : tabId,
  402. text : ""
  403. });
  404. if (tabs[tabId] && tabs[tabId].processing) {
  405. sendResponse({
  406. processing : true
  407. });
  408. return;
  409. }
  410. if (tabs[tabId] && tabs[tabId].processed) {
  411. sendResponse({
  412. processed : true
  413. });
  414. return;
  415. }
  416. processTab(tabId, true);
  417. sendResponse({});
  418. });
  419. })();